# This PR introduces:
## Parsing arrays:
{1,2,3} and {1;2;3}
Note that array elements can be numbers, booleans and errors (#VALUE!)
## Evaluating arrays in the SUM function
=SUM({1,2,3}) works!
## Evaluating arithmetic operation with arrays
=SUM({1,2,3} * 8) or =SUM({1,2,3}+{2,4,5}) works
This is done with just one function (handle_arithmetic) for most operations
## Some mathematical functions implement arrays
=SUM(SIN({1,2,3})) works
This is done with macros. See fn_single_number
So that implementing new functions that supports array are easy
# Not done in this PR
## Most functions are not supporting arrays
When that happens we either through #N/IMPL! (not implemented error)
or do implicit intersection. Some functions will be rather trivial to "arraify" some will be hard
## The final result in a cell cannot be an array
The formula ={1,2,3} in a cell will result in #N/IMPL!
## Exporting arrays to Excel might not work correctly
Excel uses the cm (cell metadata) for formulas that contain dynamic arrays.
Although the present PR does not introduce dynamic arrays some formulas like =SUM(SIN({1,2,3}))
is considered a dynamic formula
## There are not a lot of tests in this delivery
The bulk of the tests will be added once we start going function by function# This PR introduces:
## Parsing arrays:
{1,2,3} and {1;2;3}
Note that array elements can be numbers, booleans and errors (#VALUE!)
## Evaluating arrays in the SUM function
=SUM({1,2,3}) works!
## Evaluating arithmetic operation with arrays
=SUM({1,2,3} * 8) or =SUM({1,2,3}+{2,4,5}) works
This is done with just one function (handle_arithmetic) for most operations
## Some mathematical functions implement arrays
=SUM(SIN({1,2,3})) works
This is done with macros. See fn_single_number
So that implementing new functions that supports array are easy
# Not done in this PR
## Most functions are not supporting arrays
When that happens we either through #N/IMPL! (not implemented error)
or do implicit intersection. Some functions will be rather trivial to "arraify" some will be hard
## The final result in a cell cannot be an array
The formula ={1,2,3} in a cell will result in #N/IMPL!
## Exporting arrays to Excel might not work correctly
Excel uses the cm (cell metadata) for formulas that contain dynamic arrays.
Although the present PR does not introduce dynamic arrays some formulas like =SUM(SIN({1,2,3}))
is considered a dynamic formula
## There are not a lot of tests in this delivery
The bulk of the tests will be added once we start going function by function
## The array parsing does not respect the locale
Locales that use ',' as a decimal separator need to use something different for arrays
## The might introduce a small performance penalty
We haven't been benchmarking, and having closures for every arithmetic operation and every function
evaluation will introduce a performance hit. Fixing that in he future is not so hard writing tailored
code for the operation
1276 lines
49 KiB
Rust
1276 lines
49 KiB
Rust
use crate::{
|
|
calc_result::CalcResult,
|
|
constants::{LAST_COLUMN, LAST_ROW},
|
|
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
|
|
formatter::format::{format_number, parse_formatted_number},
|
|
model::Model,
|
|
number_format::to_precision,
|
|
};
|
|
|
|
use super::{
|
|
text_util::{substitute, text_after, text_before, Case},
|
|
util::from_wildcard_to_regex,
|
|
};
|
|
|
|
/// Finds the first instance of 'search_for' in text starting at char index start
|
|
fn find(search_for: &str, text: &str, start: usize) -> Option<i32> {
|
|
let ch = text.chars();
|
|
let mut byte_index = 0;
|
|
for (char_index, c) in ch.enumerate() {
|
|
if char_index + 1 >= start && text[byte_index..].starts_with(search_for) {
|
|
return Some((char_index + 1) as i32);
|
|
}
|
|
byte_index += c.len_utf8();
|
|
}
|
|
None
|
|
}
|
|
|
|
/// You can use the wildcard characters — the question mark (?) and asterisk (*) — in the find_text argument.
|
|
/// * A question mark matches any single character.
|
|
/// * An asterisk matches any sequence of characters.
|
|
/// * If you want to find an actual question mark or asterisk, type a tilde (~) before the character.
|
|
fn search(search_for: &str, text: &str, start: usize) -> Option<i32> {
|
|
let re = match from_wildcard_to_regex(search_for, false) {
|
|
Ok(r) => r,
|
|
Err(_) => return None,
|
|
};
|
|
|
|
let ch = text.chars();
|
|
let mut byte_index = 0;
|
|
for (char_index, c) in ch.enumerate() {
|
|
if char_index + 1 >= start {
|
|
if let Some(m) = re.find(&text[byte_index..]) {
|
|
return Some((text[0..(m.start() + byte_index)].chars().count() as i32) + 1);
|
|
} else {
|
|
return None;
|
|
}
|
|
}
|
|
byte_index += c.len_utf8();
|
|
}
|
|
None
|
|
}
|
|
|
|
impl Model {
|
|
pub(crate) fn fn_concat(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let mut result = "".to_string();
|
|
for arg in args {
|
|
match self.evaluate_node_in_context(arg, cell) {
|
|
CalcResult::String(value) => result = format!("{}{}", result, value),
|
|
CalcResult::Number(value) => result = format!("{}{}", result, value),
|
|
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
|
CalcResult::Boolean(value) => {
|
|
if value {
|
|
result = format!("{}TRUE", result);
|
|
} else {
|
|
result = format!("{}FALSE", result);
|
|
}
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::Range { left, right } => {
|
|
if left.sheet != right.sheet {
|
|
return CalcResult::new_error(
|
|
Error::VALUE,
|
|
cell,
|
|
"Ranges are in different sheets".to_string(),
|
|
);
|
|
}
|
|
for row in left.row..(right.row + 1) {
|
|
for column in left.column..(right.column + 1) {
|
|
match self.evaluate_cell(CellReferenceIndex {
|
|
sheet: left.sheet,
|
|
row,
|
|
column,
|
|
}) {
|
|
CalcResult::String(value) => {
|
|
result = format!("{}{}", result, value);
|
|
}
|
|
CalcResult::Number(value) => {
|
|
result = format!("{}{}", result, value)
|
|
}
|
|
CalcResult::Boolean(value) => {
|
|
if value {
|
|
result = format!("{}TRUE", result);
|
|
} else {
|
|
result = format!("{}FALSE", result);
|
|
}
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
|
CalcResult::Range { .. } => {}
|
|
CalcResult::Array(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
CalcResult::Array(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
}
|
|
}
|
|
};
|
|
}
|
|
CalcResult::String(result)
|
|
}
|
|
pub(crate) fn fn_text(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() == 2 {
|
|
let value = match self.evaluate_node_in_context(&args[0], cell) {
|
|
CalcResult::Number(f) => f,
|
|
CalcResult::String(s) => {
|
|
return CalcResult::String(s);
|
|
}
|
|
CalcResult::Boolean(b) => {
|
|
return CalcResult::Boolean(b);
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::Range { .. } => {
|
|
// Implicit Intersection not implemented
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Implicit Intersection not implemented".to_string(),
|
|
};
|
|
}
|
|
CalcResult::EmptyCell | CalcResult::EmptyArg => 0.0,
|
|
CalcResult::Array(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
}
|
|
}
|
|
};
|
|
let format_code = match self.get_string(&args[1], cell) {
|
|
Ok(s) => s,
|
|
Err(s) => return s,
|
|
};
|
|
let d = format_number(value, &format_code, &self.locale);
|
|
if let Some(_e) = d.error {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Invalid format code".to_string(),
|
|
};
|
|
}
|
|
CalcResult::String(d.text)
|
|
} else {
|
|
CalcResult::new_args_number_error(cell)
|
|
}
|
|
}
|
|
|
|
/// FIND(find_text, within_text, [start_num])
|
|
/// * FIND and FINDB are case sensitive and don't allow wildcard characters.
|
|
/// * If find_text is "" (empty text), FIND matches the first character in the search string (that is, the character numbered start_num or 1).
|
|
/// * Find_text cannot contain any wildcard characters.
|
|
/// * If find_text does not appear in within_text, FIND and FINDB return the #VALUE! error value.
|
|
/// * If start_num is not greater than zero, FIND and FINDB return the #VALUE! error value.
|
|
/// * If start_num is greater than the length of within_text, FIND and FINDB return the #VALUE! error value.
|
|
/// NB: FINDB is not implemented. It is the same as FIND function unless locale is a DBCS (Double Byte Character Set)
|
|
pub(crate) fn fn_find(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() < 2 || args.len() > 3 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let find_text = match self.get_string(&args[0], cell) {
|
|
Ok(s) => s,
|
|
Err(s) => return s,
|
|
};
|
|
let within_text = match self.get_string(&args[1], cell) {
|
|
Ok(s) => s,
|
|
Err(s) => return s,
|
|
};
|
|
let start_num = if args.len() == 3 {
|
|
match self.get_number(&args[2], cell) {
|
|
Ok(s) => s.floor(),
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
1.0
|
|
};
|
|
|
|
if start_num < 1.0 {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Start num must be >= 1".to_string(),
|
|
};
|
|
}
|
|
let start_num = start_num as usize;
|
|
|
|
if start_num > within_text.len() {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Start num greater than length".to_string(),
|
|
};
|
|
}
|
|
if let Some(s) = find(&find_text, &within_text, start_num) {
|
|
CalcResult::Number(s as f64)
|
|
} else {
|
|
CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Text not found".to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Same API as FIND but:
|
|
/// * Allows wildcards
|
|
/// * It is case insensitive
|
|
/// SEARCH(find_text, within_text, [start_num])
|
|
pub(crate) fn fn_search(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() < 2 || args.len() > 3 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let find_text = match self.get_string(&args[0], cell) {
|
|
Ok(s) => s,
|
|
Err(s) => return s,
|
|
};
|
|
let within_text = match self.get_string(&args[1], cell) {
|
|
Ok(s) => s,
|
|
Err(s) => return s,
|
|
};
|
|
let start_num = if args.len() == 3 {
|
|
match self.get_number(&args[2], cell) {
|
|
Ok(s) => s.floor(),
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
1.0
|
|
};
|
|
|
|
if start_num < 1.0 {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Start num must be >= 1".to_string(),
|
|
};
|
|
}
|
|
let start_num = start_num as usize;
|
|
|
|
if start_num > within_text.len() {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Start num greater than length".to_string(),
|
|
};
|
|
}
|
|
// SEARCH is case insensitive
|
|
if let Some(s) = search(
|
|
&find_text.to_lowercase(),
|
|
&within_text.to_lowercase(),
|
|
start_num,
|
|
) {
|
|
CalcResult::Number(s as f64)
|
|
} else {
|
|
CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Text not found".to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
// LEN, LEFT, RIGHT, MID, LOWER, UPPER, TRIM
|
|
pub(crate) fn fn_len(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() == 1 {
|
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
|
CalcResult::Number(v) => format!("{}", v),
|
|
CalcResult::String(v) => v,
|
|
CalcResult::Boolean(b) => {
|
|
if b {
|
|
"TRUE".to_string()
|
|
} else {
|
|
"FALSE".to_string()
|
|
}
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::Range { .. } => {
|
|
// Implicit Intersection not implemented
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Implicit Intersection not implemented".to_string(),
|
|
};
|
|
}
|
|
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
|
CalcResult::Array(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
}
|
|
}
|
|
};
|
|
return CalcResult::Number(s.chars().count() as f64);
|
|
}
|
|
CalcResult::new_args_number_error(cell)
|
|
}
|
|
|
|
pub(crate) fn fn_trim(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() == 1 {
|
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
|
CalcResult::Number(v) => format!("{}", v),
|
|
CalcResult::String(v) => v,
|
|
CalcResult::Boolean(b) => {
|
|
if b {
|
|
"TRUE".to_string()
|
|
} else {
|
|
"FALSE".to_string()
|
|
}
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::Range { .. } => {
|
|
// Implicit Intersection not implemented
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Implicit Intersection not implemented".to_string(),
|
|
};
|
|
}
|
|
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
|
CalcResult::Array(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
}
|
|
}
|
|
};
|
|
return CalcResult::String(s.trim().to_owned());
|
|
}
|
|
CalcResult::new_args_number_error(cell)
|
|
}
|
|
|
|
pub(crate) fn fn_lower(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() == 1 {
|
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
|
CalcResult::Number(v) => format!("{}", v),
|
|
CalcResult::String(v) => v,
|
|
CalcResult::Boolean(b) => {
|
|
if b {
|
|
"TRUE".to_string()
|
|
} else {
|
|
"FALSE".to_string()
|
|
}
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::Range { .. } => {
|
|
// Implicit Intersection not implemented
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Implicit Intersection not implemented".to_string(),
|
|
};
|
|
}
|
|
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
|
CalcResult::Array(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
}
|
|
}
|
|
};
|
|
return CalcResult::String(s.to_lowercase());
|
|
}
|
|
CalcResult::new_args_number_error(cell)
|
|
}
|
|
|
|
pub(crate) fn fn_unicode(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() == 1 {
|
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
|
CalcResult::Number(v) => format!("{}", v),
|
|
CalcResult::String(v) => v,
|
|
CalcResult::Boolean(b) => {
|
|
if b {
|
|
"TRUE".to_string()
|
|
} else {
|
|
"FALSE".to_string()
|
|
}
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::Range { .. } => {
|
|
// Implicit Intersection not implemented
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Implicit Intersection not implemented".to_string(),
|
|
};
|
|
}
|
|
CalcResult::EmptyCell | CalcResult::EmptyArg => {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Empty cell".to_string(),
|
|
}
|
|
}
|
|
CalcResult::Array(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
}
|
|
}
|
|
};
|
|
|
|
match s.chars().next() {
|
|
Some(c) => {
|
|
let unicode_number = c as u32;
|
|
return CalcResult::Number(unicode_number as f64);
|
|
}
|
|
None => {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Empty cell".to_string(),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
CalcResult::new_args_number_error(cell)
|
|
}
|
|
|
|
pub(crate) fn fn_upper(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() == 1 {
|
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
|
CalcResult::Number(v) => format!("{}", v),
|
|
CalcResult::String(v) => v,
|
|
CalcResult::Boolean(b) => {
|
|
if b {
|
|
"TRUE".to_string()
|
|
} else {
|
|
"FALSE".to_string()
|
|
}
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::Range { .. } => {
|
|
// Implicit Intersection not implemented
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Implicit Intersection not implemented".to_string(),
|
|
};
|
|
}
|
|
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
|
CalcResult::Array(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
}
|
|
}
|
|
};
|
|
return CalcResult::String(s.to_uppercase());
|
|
}
|
|
CalcResult::new_args_number_error(cell)
|
|
}
|
|
|
|
pub(crate) fn fn_left(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() > 2 || args.is_empty() {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
|
CalcResult::Number(v) => format!("{}", v),
|
|
CalcResult::String(v) => v,
|
|
CalcResult::Boolean(b) => {
|
|
if b {
|
|
"TRUE".to_string()
|
|
} else {
|
|
"FALSE".to_string()
|
|
}
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::Range { .. } => {
|
|
// Implicit Intersection not implemented
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Implicit Intersection not implemented".to_string(),
|
|
};
|
|
}
|
|
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
|
CalcResult::Array(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
}
|
|
}
|
|
};
|
|
let num_chars = if args.len() == 2 {
|
|
match self.evaluate_node_in_context(&args[1], cell) {
|
|
CalcResult::Number(v) => {
|
|
if v < 0.0 {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Number must be >= 0".to_string(),
|
|
};
|
|
}
|
|
v.floor() as usize
|
|
}
|
|
CalcResult::Boolean(_) | CalcResult::String(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Expecting number".to_string(),
|
|
};
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::Range { .. } => {
|
|
// Implicit Intersection not implemented
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Implicit Intersection not implemented".to_string(),
|
|
};
|
|
}
|
|
CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
|
|
CalcResult::Array(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
1
|
|
};
|
|
let mut result = "".to_string();
|
|
for (index, ch) in s.chars().enumerate() {
|
|
if index >= num_chars {
|
|
break;
|
|
}
|
|
result.push(ch);
|
|
}
|
|
CalcResult::String(result)
|
|
}
|
|
|
|
pub(crate) fn fn_right(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() > 2 || args.is_empty() {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
|
CalcResult::Number(v) => format!("{}", v),
|
|
CalcResult::String(v) => v,
|
|
CalcResult::Boolean(b) => {
|
|
if b {
|
|
"TRUE".to_string()
|
|
} else {
|
|
"FALSE".to_string()
|
|
}
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::Range { .. } => {
|
|
// Implicit Intersection not implemented
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Implicit Intersection not implemented".to_string(),
|
|
};
|
|
}
|
|
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
|
CalcResult::Array(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
}
|
|
}
|
|
};
|
|
let num_chars = if args.len() == 2 {
|
|
match self.evaluate_node_in_context(&args[1], cell) {
|
|
CalcResult::Number(v) => {
|
|
if v < 0.0 {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Number must be >= 0".to_string(),
|
|
};
|
|
}
|
|
v.floor() as usize
|
|
}
|
|
CalcResult::Boolean(_) | CalcResult::String(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Expecting number".to_string(),
|
|
};
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::Range { .. } => {
|
|
// Implicit Intersection not implemented
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Implicit Intersection not implemented".to_string(),
|
|
};
|
|
}
|
|
CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
|
|
CalcResult::Array(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
1
|
|
};
|
|
let mut result = "".to_string();
|
|
for (index, ch) in s.chars().rev().enumerate() {
|
|
if index >= num_chars {
|
|
break;
|
|
}
|
|
result.push(ch);
|
|
}
|
|
CalcResult::String(result.chars().rev().collect::<String>())
|
|
}
|
|
|
|
pub(crate) fn fn_mid(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() != 3 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
|
CalcResult::Number(v) => format!("{}", v),
|
|
CalcResult::String(v) => v,
|
|
CalcResult::Boolean(b) => {
|
|
if b {
|
|
"TRUE".to_string()
|
|
} else {
|
|
"FALSE".to_string()
|
|
}
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::Range { .. } => {
|
|
// Implicit Intersection not implemented
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Implicit Intersection not implemented".to_string(),
|
|
};
|
|
}
|
|
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
|
|
CalcResult::Array(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
}
|
|
}
|
|
};
|
|
let start_num = match self.evaluate_node_in_context(&args[1], cell) {
|
|
CalcResult::Number(v) => {
|
|
if v < 1.0 {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Number must be >= 1".to_string(),
|
|
};
|
|
}
|
|
v.floor() as usize
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::Range { .. } => {
|
|
// Implicit Intersection not implemented
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Implicit Intersection not implemented".to_string(),
|
|
};
|
|
}
|
|
_ => {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Expecting number".to_string(),
|
|
};
|
|
}
|
|
};
|
|
let num_chars = match self.evaluate_node_in_context(&args[2], cell) {
|
|
CalcResult::Number(v) => {
|
|
if v < 0.0 {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Number must be >= 0".to_string(),
|
|
};
|
|
}
|
|
v.floor() as usize
|
|
}
|
|
CalcResult::String(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Expecting number".to_string(),
|
|
};
|
|
}
|
|
CalcResult::Boolean(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Expecting number".to_string(),
|
|
}
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::Range { .. } => {
|
|
// Implicit Intersection not implemented
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Implicit Intersection not implemented".to_string(),
|
|
};
|
|
}
|
|
CalcResult::EmptyCell | CalcResult::EmptyArg => 0,
|
|
CalcResult::Array(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
}
|
|
}
|
|
};
|
|
let mut result = "".to_string();
|
|
let mut count: usize = 0;
|
|
for (index, ch) in s.chars().enumerate() {
|
|
if count >= num_chars {
|
|
break;
|
|
}
|
|
if index + 1 >= start_num {
|
|
result.push(ch);
|
|
count += 1;
|
|
}
|
|
}
|
|
CalcResult::String(result)
|
|
}
|
|
|
|
// REPT(text, number_times)
|
|
pub(crate) fn fn_rept(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() != 2 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let text = match self.get_string(&args[0], cell) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
let number_times = match self.get_number(&args[1], cell) {
|
|
Ok(f) => f.floor() as i32,
|
|
Err(s) => return s,
|
|
};
|
|
let text_len = text.len() as i32;
|
|
|
|
// We normally don't follow Excel's sometimes archaic size's restrictions
|
|
// But this might be a security issue
|
|
if text_len * number_times > 32767 {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "number times too high".to_string(),
|
|
};
|
|
}
|
|
if number_times < 0 {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "number times too high".to_string(),
|
|
};
|
|
}
|
|
if number_times == 0 {
|
|
return CalcResult::String("".to_string());
|
|
}
|
|
CalcResult::String(text.repeat(number_times as usize))
|
|
}
|
|
|
|
// TEXTAFTER(text, delimiter, [instance_num], [match_mode], [match_end], [if_not_found])
|
|
pub(crate) fn fn_textafter(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if !(2..=6).contains(&arg_count) {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let text = match self.get_string(&args[0], cell) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
let delimiter = match self.get_string(&args[1], cell) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
let instance_num = if arg_count > 2 {
|
|
match self.get_number(&args[2], cell) {
|
|
Ok(f) => f.floor() as i32,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
1
|
|
};
|
|
let match_mode = if arg_count > 3 {
|
|
match self.get_number(&args[3], cell) {
|
|
Ok(f) => {
|
|
if f == 0.0 {
|
|
Case::Sensitive
|
|
} else {
|
|
Case::Insensitive
|
|
}
|
|
}
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
Case::Sensitive
|
|
};
|
|
|
|
let match_end = if arg_count > 4 {
|
|
match self.get_number(&args[4], cell) {
|
|
Ok(f) => f,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
// disabled by default
|
|
// the delimiter is specified in the formula
|
|
0.0
|
|
};
|
|
if instance_num == 0 {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "instance_num must be <> 0".to_string(),
|
|
};
|
|
}
|
|
if delimiter.len() > text.len() {
|
|
// so this is fun(!)
|
|
// if the function was provided with two arguments is a #VALUE!
|
|
// if it had more is a #N/A (irrespective of their values)
|
|
if arg_count > 2 {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "The delimiter is longer than the text is trying to match".to_string(),
|
|
};
|
|
} else {
|
|
return CalcResult::Error {
|
|
error: Error::NA,
|
|
origin: cell,
|
|
message: "The delimiter is longer than the text is trying to match".to_string(),
|
|
};
|
|
}
|
|
}
|
|
if match_end != 0.0 && match_end != 1.0 {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "argument must be 0 or 1".to_string(),
|
|
};
|
|
};
|
|
match text_after(&text, &delimiter, instance_num, match_mode) {
|
|
Some(s) => CalcResult::String(s),
|
|
None => {
|
|
if match_end == 1.0 {
|
|
if instance_num == 1 {
|
|
return CalcResult::String("".to_string());
|
|
} else if instance_num == -1 {
|
|
return CalcResult::String(text);
|
|
}
|
|
}
|
|
if arg_count == 6 {
|
|
// An empty cell is converted to empty string (not 0)
|
|
match self.evaluate_node_in_context(&args[5], cell) {
|
|
CalcResult::EmptyCell => CalcResult::String("".to_string()),
|
|
result => result,
|
|
}
|
|
} else {
|
|
CalcResult::Error {
|
|
error: Error::NA,
|
|
origin: cell,
|
|
message: "Value not found".to_string(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn fn_textbefore(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if !(2..=6).contains(&arg_count) {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let text = match self.get_string(&args[0], cell) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
let delimiter = match self.get_string(&args[1], cell) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
let instance_num = if arg_count > 2 {
|
|
match self.get_number(&args[2], cell) {
|
|
Ok(f) => f.floor() as i32,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
1
|
|
};
|
|
let match_mode = if arg_count > 3 {
|
|
match self.get_number(&args[3], cell) {
|
|
Ok(f) => {
|
|
if f == 0.0 {
|
|
Case::Sensitive
|
|
} else {
|
|
Case::Insensitive
|
|
}
|
|
}
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
Case::Sensitive
|
|
};
|
|
|
|
let match_end = if arg_count > 4 {
|
|
match self.get_number(&args[4], cell) {
|
|
Ok(f) => f,
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
// disabled by default
|
|
// the delimiter is specified in the formula
|
|
0.0
|
|
};
|
|
if instance_num == 0 {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "instance_num must be <> 0".to_string(),
|
|
};
|
|
}
|
|
if delimiter.len() > text.len() {
|
|
// so this is fun(!)
|
|
// if the function was provided with two arguments is a #VALUE!
|
|
// if it had more is a #N/A (irrespective of their values)
|
|
if arg_count > 2 {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "The delimiter is longer than the text is trying to match".to_string(),
|
|
};
|
|
} else {
|
|
return CalcResult::Error {
|
|
error: Error::NA,
|
|
origin: cell,
|
|
message: "The delimiter is longer than the text is trying to match".to_string(),
|
|
};
|
|
}
|
|
}
|
|
if match_end != 0.0 && match_end != 1.0 {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "argument must be 0 or 1".to_string(),
|
|
};
|
|
};
|
|
match text_before(&text, &delimiter, instance_num, match_mode) {
|
|
Some(s) => CalcResult::String(s),
|
|
None => {
|
|
if match_end == 1.0 {
|
|
if instance_num == -1 {
|
|
return CalcResult::String("".to_string());
|
|
} else if instance_num == 1 {
|
|
return CalcResult::String(text);
|
|
}
|
|
}
|
|
if arg_count == 6 {
|
|
// An empty cell is converted to empty string (not 0)
|
|
match self.evaluate_node_in_context(&args[5], cell) {
|
|
CalcResult::EmptyCell => CalcResult::String("".to_string()),
|
|
result => result,
|
|
}
|
|
} else {
|
|
CalcResult::Error {
|
|
error: Error::NA,
|
|
origin: cell,
|
|
message: "Value not found".to_string(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TEXTJOIN(delimiter, ignore_empty, text1, [text2], …)
|
|
pub(crate) fn fn_textjoin(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if arg_count < 3 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let delimiter = match self.get_string(&args[0], cell) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
let ignore_empty = match self.get_boolean(&args[1], cell) {
|
|
Ok(b) => b,
|
|
Err(error) => return error,
|
|
};
|
|
let mut values = Vec::new();
|
|
for arg in &args[2..] {
|
|
match self.evaluate_node_in_context(arg, cell) {
|
|
CalcResult::Number(value) => values.push(format!("{value}")),
|
|
CalcResult::Range { left, right } => {
|
|
if left.sheet != right.sheet {
|
|
return CalcResult::new_error(
|
|
Error::VALUE,
|
|
cell,
|
|
"Ranges are in different sheets".to_string(),
|
|
);
|
|
}
|
|
let row1 = left.row;
|
|
let mut row2 = right.row;
|
|
let column1 = left.column;
|
|
let mut column2 = right.column;
|
|
if row1 == 1 && row2 == LAST_ROW {
|
|
row2 = match self.workbook.worksheet(left.sheet) {
|
|
Ok(s) => s.dimension().max_row,
|
|
Err(_) => {
|
|
return CalcResult::new_error(
|
|
Error::ERROR,
|
|
cell,
|
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
|
);
|
|
}
|
|
};
|
|
}
|
|
if column1 == 1 && column2 == LAST_COLUMN {
|
|
column2 = match self.workbook.worksheet(left.sheet) {
|
|
Ok(s) => s.dimension().max_column,
|
|
Err(_) => {
|
|
return CalcResult::new_error(
|
|
Error::ERROR,
|
|
cell,
|
|
format!("Invalid worksheet index: '{}'", left.sheet),
|
|
);
|
|
}
|
|
};
|
|
}
|
|
for row in row1..row2 + 1 {
|
|
for column in column1..(column2 + 1) {
|
|
match self.evaluate_cell(CellReferenceIndex {
|
|
sheet: left.sheet,
|
|
row,
|
|
column,
|
|
}) {
|
|
CalcResult::Number(value) => {
|
|
values.push(format!("{value}"));
|
|
}
|
|
CalcResult::String(value) => values.push(value),
|
|
CalcResult::Boolean(value) => {
|
|
if value {
|
|
values.push("TRUE".to_string())
|
|
} else {
|
|
values.push("FALSE".to_string())
|
|
}
|
|
}
|
|
CalcResult::EmptyCell => {
|
|
if !ignore_empty {
|
|
values.push("".to_string())
|
|
}
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::EmptyArg | CalcResult::Range { .. } => {}
|
|
CalcResult::Array(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
error @ CalcResult::Error { .. } => return error,
|
|
CalcResult::String(value) => values.push(value),
|
|
CalcResult::Boolean(value) => {
|
|
if value {
|
|
values.push("TRUE".to_string())
|
|
} else {
|
|
values.push("FALSE".to_string())
|
|
}
|
|
}
|
|
CalcResult::EmptyCell => {
|
|
if !ignore_empty {
|
|
values.push("".to_string())
|
|
}
|
|
}
|
|
CalcResult::EmptyArg => {}
|
|
CalcResult::Array(_) => {
|
|
return CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
}
|
|
}
|
|
};
|
|
}
|
|
let result = values.join(&delimiter);
|
|
CalcResult::String(result)
|
|
}
|
|
|
|
// SUBSTITUTE(text, old_text, new_text, [instance_num])
|
|
pub(crate) fn fn_substitute(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if !(2..=4).contains(&arg_count) {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let text = match self.get_string(&args[0], cell) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
let old_text = match self.get_string(&args[1], cell) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
let new_text = match self.get_string(&args[2], cell) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
let instance_num = if arg_count > 3 {
|
|
match self.get_number(&args[3], cell) {
|
|
Ok(f) => Some(f.floor() as i32),
|
|
Err(s) => return s,
|
|
}
|
|
} else {
|
|
// means every instance is replaced
|
|
None
|
|
};
|
|
if let Some(num) = instance_num {
|
|
if num < 1 {
|
|
return CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Invalid value".to_string(),
|
|
};
|
|
}
|
|
if old_text.is_empty() {
|
|
return CalcResult::String(text);
|
|
}
|
|
CalcResult::String(substitute(&text, &old_text, &new_text, num))
|
|
} else {
|
|
if old_text.is_empty() {
|
|
return CalcResult::String(text);
|
|
}
|
|
CalcResult::String(text.replace(&old_text, &new_text))
|
|
}
|
|
}
|
|
pub(crate) fn fn_concatenate(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
let arg_count = args.len();
|
|
if arg_count == 0 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let mut text_array = Vec::new();
|
|
for arg in args {
|
|
let text = match self.get_string(arg, cell) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
text_array.push(text)
|
|
}
|
|
CalcResult::String(text_array.join(""))
|
|
}
|
|
|
|
pub(crate) fn fn_exact(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() != 2 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let result1 = &self.evaluate_node_in_context(&args[0], cell);
|
|
let result2 = &self.evaluate_node_in_context(&args[1], cell);
|
|
// FIXME: Implicit intersection
|
|
if let (CalcResult::Number(number1), CalcResult::Number(number2)) = (result1, result2) {
|
|
// In Excel two numbers are the same if they are the same up to 15 digits.
|
|
CalcResult::Boolean(to_precision(*number1, 15) == to_precision(*number2, 15))
|
|
} else {
|
|
let string1 = match self.cast_to_string(result1.clone(), cell) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
let string2 = match self.cast_to_string(result2.clone(), cell) {
|
|
Ok(s) => s,
|
|
Err(error) => return error,
|
|
};
|
|
CalcResult::Boolean(string1 == string2)
|
|
}
|
|
}
|
|
// VALUE(text)
|
|
pub(crate) fn fn_value(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() != 1 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
match self.evaluate_node_in_context(&args[0], cell) {
|
|
CalcResult::String(text) => {
|
|
let currencies = vec!["$", "€"];
|
|
if let Ok((value, _)) = parse_formatted_number(&text, ¤cies) {
|
|
return CalcResult::Number(value);
|
|
};
|
|
CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Invalid number".to_string(),
|
|
}
|
|
}
|
|
CalcResult::Number(f) => CalcResult::Number(f),
|
|
CalcResult::Boolean(_) => CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Invalid number".to_string(),
|
|
},
|
|
error @ CalcResult::Error { .. } => error,
|
|
CalcResult::Range { .. } => {
|
|
// TODO Implicit Intersection
|
|
CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Invalid number".to_string(),
|
|
}
|
|
}
|
|
CalcResult::EmptyCell | CalcResult::EmptyArg => CalcResult::Number(0.0),
|
|
CalcResult::Array(_) => CalcResult::Error {
|
|
error: Error::NIMPL,
|
|
origin: cell,
|
|
message: "Arrays not supported yet".to_string(),
|
|
},
|
|
}
|
|
}
|
|
|
|
pub(crate) fn fn_t(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() != 1 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
// FIXME: Implicit intersection
|
|
let result = self.evaluate_node_in_context(&args[0], cell);
|
|
match result {
|
|
CalcResult::String(_) => result,
|
|
error @ CalcResult::Error { .. } => error,
|
|
_ => CalcResult::String("".to_string()),
|
|
}
|
|
}
|
|
|
|
// VALUETOTEXT(value)
|
|
pub(crate) fn fn_valuetotext(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
|
if args.len() != 1 {
|
|
return CalcResult::new_args_number_error(cell);
|
|
}
|
|
let text = match self.get_string(&args[0], cell) {
|
|
Ok(s) => s,
|
|
Err(error) => match error {
|
|
CalcResult::Error { error, .. } => error.to_string(),
|
|
_ => "".to_string(),
|
|
},
|
|
};
|
|
CalcResult::String(text)
|
|
}
|
|
}
|