1921 lines
70 KiB
Rust
1921 lines
70 KiB
Rust
#![deny(missing_docs)]
|
|
|
|
//! # Model
|
|
//!
|
|
//! Note that sheets are 0-indexed and rows and columns are 1-indexed.
|
|
//!
|
|
//! IronCalc is row first. A cell is referenced by (`sheet`, `row`, `column`)
|
|
//!
|
|
|
|
use bincode::config;
|
|
use serde_json::json;
|
|
|
|
use std::collections::HashMap;
|
|
use std::vec::Vec;
|
|
|
|
use crate::{
|
|
calc_result::{CalcResult, Range},
|
|
cell::CellValue,
|
|
constants::{self, LAST_COLUMN, LAST_ROW},
|
|
expressions::token::{Error, OpCompare, OpProduct, OpSum, OpUnary},
|
|
expressions::{
|
|
parser::move_formula::{move_formula, MoveContext},
|
|
token::get_error_by_name,
|
|
types::*,
|
|
utils::{self, is_valid_row},
|
|
},
|
|
expressions::{
|
|
parser::{
|
|
stringify::{to_rc_format, to_string},
|
|
Node, Parser,
|
|
},
|
|
utils::is_valid_column_number,
|
|
},
|
|
formatter::{
|
|
format::{format_number, parse_formatted_number},
|
|
lexer::is_likely_date_number_format,
|
|
},
|
|
functions::util::compare_values,
|
|
implicit_intersection::implicit_intersection,
|
|
language::{get_language, Language},
|
|
locale::{get_locale, Currency, Locale},
|
|
types::*,
|
|
utils as common,
|
|
};
|
|
|
|
use chrono_tz::Tz;
|
|
|
|
#[cfg(test)]
|
|
pub use crate::mock_time::get_milliseconds_since_epoch;
|
|
|
|
/// wasm implementation for time
|
|
#[cfg(not(test))]
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub fn get_milliseconds_since_epoch() -> i64 {
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.expect("problem with system time")
|
|
.as_millis() as i64
|
|
}
|
|
|
|
#[cfg(not(test))]
|
|
#[cfg(target_arch = "wasm32")]
|
|
pub fn get_milliseconds_since_epoch() -> i64 {
|
|
use js_sys::Date;
|
|
Date::now() as i64
|
|
}
|
|
|
|
/// A cell might be evaluated or being evaluated
|
|
#[derive(Clone)]
|
|
pub enum CellState {
|
|
/// The cell has already been evaluated
|
|
Evaluated,
|
|
/// The cell is being evaluated
|
|
Evaluating,
|
|
}
|
|
|
|
/// A parsed formula for a defined name
|
|
pub enum ParsedDefinedName {
|
|
/// CellReference (`=C4`)
|
|
CellReference(CellReferenceIndex),
|
|
/// A Range (`=C4:D6`)
|
|
RangeReference(Range),
|
|
/// `=SomethingElse`
|
|
InvalidDefinedNameFormula,
|
|
// TODO: Support constants in defined names
|
|
// TODO: Support formulas in defined names
|
|
// TODO: Support tables in defined names
|
|
}
|
|
|
|
/// A dynamical IronCalc model.
|
|
///
|
|
/// Its is composed of a `Workbook`. Everything else are dynamical quantities:
|
|
///
|
|
/// * The Locale: a parsed version of the Workbook's locale
|
|
/// * The Timezone: an object representing the Workbook's timezone
|
|
/// * The language. Note that the timezone and the locale belong to the workbook while
|
|
/// the language can be different for different users looking _at the same_ workbook.
|
|
/// * Parsed Formulas: All the formulas in the workbook are parsed here (runtime only)
|
|
/// * A list of cells with its status (evaluating, evaluated, not evaluated)
|
|
/// * A dictionary with the shared strings and their indices.
|
|
/// This is an optimization for large files (~1 million rows)
|
|
pub struct Model {
|
|
/// A Rust internal representation of an Excel workbook
|
|
pub workbook: Workbook,
|
|
/// A list of parsed formulas
|
|
pub parsed_formulas: Vec<Vec<Node>>,
|
|
/// A list of parsed defined names
|
|
pub parsed_defined_names: HashMap<(Option<u32>, String), ParsedDefinedName>,
|
|
/// An optimization to lookup strings faster
|
|
pub shared_strings: HashMap<String, usize>,
|
|
/// An instance of the parser
|
|
pub parser: Parser,
|
|
/// The list of cells with formulas that are evaluated of being evaluated
|
|
pub cells: HashMap<(u32, i32, i32), CellState>,
|
|
/// The locale of the model
|
|
pub locale: Locale,
|
|
/// Tha language used
|
|
pub language: Language,
|
|
/// The timezone used to evaluate the model
|
|
pub tz: Tz,
|
|
}
|
|
|
|
// FIXME: Maybe this should be the same as CellReference
|
|
/// A struct pointing to a cell
|
|
pub struct CellIndex {
|
|
/// Sheet index (0-indexed)
|
|
pub index: u32,
|
|
/// Row index
|
|
pub row: i32,
|
|
/// Column index
|
|
pub column: i32,
|
|
}
|
|
|
|
impl Model {
|
|
pub(crate) fn evaluate_node_with_reference(
|
|
&mut self,
|
|
node: &Node,
|
|
cell: CellReferenceIndex,
|
|
) -> CalcResult {
|
|
match node {
|
|
Node::ReferenceKind {
|
|
sheet_name: _,
|
|
sheet_index,
|
|
absolute_row,
|
|
absolute_column,
|
|
row,
|
|
column,
|
|
} => {
|
|
let mut row1 = *row;
|
|
let mut column1 = *column;
|
|
if !absolute_row {
|
|
row1 += cell.row;
|
|
}
|
|
if !absolute_column {
|
|
column1 += cell.column;
|
|
}
|
|
CalcResult::Range {
|
|
left: CellReferenceIndex {
|
|
sheet: *sheet_index,
|
|
row: row1,
|
|
column: column1,
|
|
},
|
|
right: CellReferenceIndex {
|
|
sheet: *sheet_index,
|
|
row: row1,
|
|
column: column1,
|
|
},
|
|
}
|
|
}
|
|
Node::RangeKind {
|
|
sheet_name: _,
|
|
sheet_index,
|
|
absolute_row1,
|
|
absolute_column1,
|
|
row1,
|
|
column1,
|
|
absolute_row2,
|
|
absolute_column2,
|
|
row2,
|
|
column2,
|
|
} => {
|
|
let mut row_left = *row1;
|
|
let mut column_left = *column1;
|
|
if !absolute_row1 {
|
|
row_left += cell.row;
|
|
}
|
|
if !absolute_column1 {
|
|
column_left += cell.column;
|
|
}
|
|
let mut row_right = *row2;
|
|
let mut column_right = *column2;
|
|
if !absolute_row2 {
|
|
row_right += cell.row;
|
|
}
|
|
if !absolute_column2 {
|
|
column_right += cell.column;
|
|
}
|
|
// FIXME: HACK. The parser is currently parsing Sheet3!A1:A10 as Sheet3!A1:(present sheet)!A10
|
|
CalcResult::Range {
|
|
left: CellReferenceIndex {
|
|
sheet: *sheet_index,
|
|
row: row_left,
|
|
column: column_left,
|
|
},
|
|
right: CellReferenceIndex {
|
|
sheet: *sheet_index,
|
|
row: row_right,
|
|
column: column_right,
|
|
},
|
|
}
|
|
}
|
|
_ => self.evaluate_node_in_context(node, cell),
|
|
}
|
|
}
|
|
|
|
fn get_range(&mut self, left: &Node, right: &Node, cell: CellReferenceIndex) -> CalcResult {
|
|
let left_result = self.evaluate_node_with_reference(left, cell);
|
|
let right_result = self.evaluate_node_with_reference(right, cell);
|
|
match (left_result, right_result) {
|
|
(
|
|
CalcResult::Range {
|
|
left: left1,
|
|
right: right1,
|
|
},
|
|
CalcResult::Range {
|
|
left: left2,
|
|
right: right2,
|
|
},
|
|
) => {
|
|
if left1.row == right1.row
|
|
&& left1.column == right1.column
|
|
&& left2.row == right2.row
|
|
&& left2.column == right2.column
|
|
{
|
|
return CalcResult::Range {
|
|
left: left1,
|
|
right: right2,
|
|
};
|
|
}
|
|
CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Invalid range".to_string(),
|
|
}
|
|
}
|
|
_ => CalcResult::Error {
|
|
error: Error::VALUE,
|
|
origin: cell,
|
|
message: "Invalid range".to_string(),
|
|
},
|
|
}
|
|
}
|
|
|
|
pub(crate) fn evaluate_node_in_context(
|
|
&mut self,
|
|
node: &Node,
|
|
cell: CellReferenceIndex,
|
|
) -> CalcResult {
|
|
use Node::*;
|
|
match node {
|
|
OpSumKind { kind, left, right } => {
|
|
// In the future once the feature try trait stabilizes we could use the '?' operator for this :)
|
|
// See: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=236044e8321a1450988e6ffe5a27dab5
|
|
let l = match self.get_number(left, cell) {
|
|
Ok(f) => f,
|
|
Err(s) => {
|
|
return s;
|
|
}
|
|
};
|
|
let r = match self.get_number(right, cell) {
|
|
Ok(f) => f,
|
|
Err(s) => {
|
|
return s;
|
|
}
|
|
};
|
|
let result = match kind {
|
|
OpSum::Add => l + r,
|
|
OpSum::Minus => l - r,
|
|
};
|
|
CalcResult::Number(result)
|
|
}
|
|
NumberKind(value) => CalcResult::Number(*value),
|
|
StringKind(value) => CalcResult::String(value.replace(r#""""#, r#"""#)),
|
|
BooleanKind(value) => CalcResult::Boolean(*value),
|
|
ReferenceKind {
|
|
sheet_name: _,
|
|
sheet_index,
|
|
absolute_row,
|
|
absolute_column,
|
|
row,
|
|
column,
|
|
} => {
|
|
let mut row1 = *row;
|
|
let mut column1 = *column;
|
|
if !absolute_row {
|
|
row1 += cell.row;
|
|
}
|
|
if !absolute_column {
|
|
column1 += cell.column;
|
|
}
|
|
self.evaluate_cell(CellReferenceIndex {
|
|
sheet: *sheet_index,
|
|
row: row1,
|
|
column: column1,
|
|
})
|
|
}
|
|
WrongReferenceKind { .. } => {
|
|
CalcResult::new_error(Error::REF, cell, "Wrong reference".to_string())
|
|
}
|
|
OpRangeKind { left, right } => self.get_range(left, right, cell),
|
|
WrongRangeKind { .. } => {
|
|
CalcResult::new_error(Error::REF, cell, "Wrong range".to_string())
|
|
}
|
|
RangeKind {
|
|
sheet_index,
|
|
row1,
|
|
column1,
|
|
row2,
|
|
column2,
|
|
absolute_column1,
|
|
absolute_row2,
|
|
absolute_row1,
|
|
absolute_column2,
|
|
sheet_name: _,
|
|
} => CalcResult::Range {
|
|
left: CellReferenceIndex {
|
|
sheet: *sheet_index,
|
|
row: if *absolute_row1 {
|
|
*row1
|
|
} else {
|
|
*row1 + cell.row
|
|
},
|
|
column: if *absolute_column1 {
|
|
*column1
|
|
} else {
|
|
*column1 + cell.column
|
|
},
|
|
},
|
|
right: CellReferenceIndex {
|
|
sheet: *sheet_index,
|
|
row: if *absolute_row2 {
|
|
*row2
|
|
} else {
|
|
*row2 + cell.row
|
|
},
|
|
column: if *absolute_column2 {
|
|
*column2
|
|
} else {
|
|
*column2 + cell.column
|
|
},
|
|
},
|
|
},
|
|
OpConcatenateKind { left, right } => {
|
|
let l = match self.get_string(left, cell) {
|
|
Ok(f) => f,
|
|
Err(s) => {
|
|
return s;
|
|
}
|
|
};
|
|
let r = match self.get_string(right, cell) {
|
|
Ok(f) => f,
|
|
Err(s) => {
|
|
return s;
|
|
}
|
|
};
|
|
let result = format!("{}{}", l, r);
|
|
CalcResult::String(result)
|
|
}
|
|
OpProductKind { kind, left, right } => {
|
|
let l = match self.get_number(left, cell) {
|
|
Ok(f) => f,
|
|
Err(s) => {
|
|
return s;
|
|
}
|
|
};
|
|
let r = match self.get_number(right, cell) {
|
|
Ok(f) => f,
|
|
Err(s) => {
|
|
return s;
|
|
}
|
|
};
|
|
let result = match kind {
|
|
OpProduct::Times => l * r,
|
|
OpProduct::Divide => {
|
|
if r == 0.0 {
|
|
return CalcResult::new_error(
|
|
Error::DIV,
|
|
cell,
|
|
"Divide by Zero".to_string(),
|
|
);
|
|
}
|
|
l / r
|
|
}
|
|
};
|
|
CalcResult::Number(result)
|
|
}
|
|
OpPowerKind { left, right } => {
|
|
let l = match self.get_number(left, cell) {
|
|
Ok(f) => f,
|
|
Err(s) => {
|
|
return s;
|
|
}
|
|
};
|
|
let r = match self.get_number(right, cell) {
|
|
Ok(f) => f,
|
|
Err(s) => {
|
|
return s;
|
|
}
|
|
};
|
|
// Deal with errors properly
|
|
CalcResult::Number(l.powf(r))
|
|
}
|
|
FunctionKind { kind, args } => self.evaluate_function(kind, args, cell),
|
|
InvalidFunctionKind { name, args: _ } => {
|
|
CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {}", name))
|
|
}
|
|
ArrayKind(_) => {
|
|
// TODO: NOT IMPLEMENTED
|
|
CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string())
|
|
}
|
|
VariableKind(defined_name) => {
|
|
let parsed_defined_name = self
|
|
.parsed_defined_names
|
|
.get(&(Some(cell.sheet), defined_name.to_lowercase())) // try getting local defined name
|
|
.or_else(|| {
|
|
self.parsed_defined_names
|
|
.get(&(None, defined_name.to_lowercase()))
|
|
}); // fallback to global
|
|
|
|
if let Some(parsed_defined_name) = parsed_defined_name {
|
|
match parsed_defined_name {
|
|
ParsedDefinedName::CellReference(reference) => {
|
|
self.evaluate_cell(*reference)
|
|
}
|
|
ParsedDefinedName::RangeReference(range) => CalcResult::Range {
|
|
left: range.left,
|
|
right: range.right,
|
|
},
|
|
ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error(
|
|
Error::NIMPL,
|
|
cell,
|
|
format!("Defined name \"{}\" is not a reference.", defined_name),
|
|
),
|
|
}
|
|
} else {
|
|
CalcResult::new_error(
|
|
Error::NAME,
|
|
cell,
|
|
format!("Defined name \"{}\" not found.", defined_name),
|
|
)
|
|
}
|
|
}
|
|
CompareKind { kind, left, right } => {
|
|
let l = self.evaluate_node_in_context(left, cell);
|
|
if l.is_error() {
|
|
return l;
|
|
}
|
|
let r = self.evaluate_node_in_context(right, cell);
|
|
if r.is_error() {
|
|
return r;
|
|
}
|
|
let compare = compare_values(&l, &r);
|
|
match kind {
|
|
OpCompare::Equal => {
|
|
if compare == 0 {
|
|
CalcResult::Boolean(true)
|
|
} else {
|
|
CalcResult::Boolean(false)
|
|
}
|
|
}
|
|
OpCompare::LessThan => {
|
|
if compare == -1 {
|
|
CalcResult::Boolean(true)
|
|
} else {
|
|
CalcResult::Boolean(false)
|
|
}
|
|
}
|
|
OpCompare::GreaterThan => {
|
|
if compare == 1 {
|
|
CalcResult::Boolean(true)
|
|
} else {
|
|
CalcResult::Boolean(false)
|
|
}
|
|
}
|
|
OpCompare::LessOrEqualThan => {
|
|
if compare < 1 {
|
|
CalcResult::Boolean(true)
|
|
} else {
|
|
CalcResult::Boolean(false)
|
|
}
|
|
}
|
|
OpCompare::GreaterOrEqualThan => {
|
|
if compare > -1 {
|
|
CalcResult::Boolean(true)
|
|
} else {
|
|
CalcResult::Boolean(false)
|
|
}
|
|
}
|
|
OpCompare::NonEqual => {
|
|
if compare != 0 {
|
|
CalcResult::Boolean(true)
|
|
} else {
|
|
CalcResult::Boolean(false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
UnaryKind { kind, right } => {
|
|
let r = match self.get_number(right, cell) {
|
|
Ok(f) => f,
|
|
Err(s) => {
|
|
return s;
|
|
}
|
|
};
|
|
match kind {
|
|
OpUnary::Minus => CalcResult::Number(-r),
|
|
OpUnary::Percentage => CalcResult::Number(r / 100.0),
|
|
}
|
|
}
|
|
ErrorKind(kind) => CalcResult::new_error(kind.clone(), cell, "".to_string()),
|
|
ParseErrorKind {
|
|
formula,
|
|
message,
|
|
position: _,
|
|
} => CalcResult::new_error(
|
|
Error::ERROR,
|
|
cell,
|
|
format!("Error parsing {}: {}", formula, message),
|
|
),
|
|
EmptyArgKind => CalcResult::EmptyArg,
|
|
}
|
|
}
|
|
|
|
fn cell_reference_to_string(
|
|
&self,
|
|
cell_reference: &CellReferenceIndex,
|
|
) -> Result<String, String> {
|
|
let sheet = self.workbook.worksheet(cell_reference.sheet)?;
|
|
let column = utils::number_to_column(cell_reference.column)
|
|
.ok_or_else(|| "Invalid column".to_string())?;
|
|
if !is_valid_row(cell_reference.row) {
|
|
return Err("Invalid row".to_string());
|
|
}
|
|
Ok(format!("{}!{}{}", sheet.name, column, cell_reference.row))
|
|
}
|
|
/// Sets `result` in the cell given by `sheet` sheet index, row and column
|
|
/// Note that will panic if the cell does not exist
|
|
/// It will do nothing if the cell does not have a formula
|
|
fn set_cell_value(&mut self, cell_reference: CellReferenceIndex, result: &CalcResult) {
|
|
let CellReferenceIndex { sheet, column, row } = cell_reference;
|
|
let cell = &self.workbook.worksheets[sheet as usize].sheet_data[&row][&column];
|
|
let s = cell.get_style();
|
|
if let Some(f) = cell.get_formula() {
|
|
match result {
|
|
CalcResult::Number(value) => {
|
|
// safety belt
|
|
if value.is_nan() || value.is_infinite() {
|
|
// This should never happen, is there a way we can log this events?
|
|
return self.set_cell_value(
|
|
cell_reference,
|
|
&CalcResult::Error {
|
|
error: Error::NUM,
|
|
origin: cell_reference,
|
|
message: "".to_string(),
|
|
},
|
|
);
|
|
}
|
|
*self.workbook.worksheets[sheet as usize]
|
|
.sheet_data
|
|
.get_mut(&row)
|
|
.expect("expected a row")
|
|
.get_mut(&column)
|
|
.expect("expected a column") = Cell::CellFormulaNumber { f, s, v: *value };
|
|
}
|
|
CalcResult::String(value) => {
|
|
*self.workbook.worksheets[sheet as usize]
|
|
.sheet_data
|
|
.get_mut(&row)
|
|
.expect("expected a row")
|
|
.get_mut(&column)
|
|
.expect("expected a column") = Cell::CellFormulaString {
|
|
f,
|
|
s,
|
|
v: value.clone(),
|
|
};
|
|
}
|
|
CalcResult::Boolean(value) => {
|
|
*self.workbook.worksheets[sheet as usize]
|
|
.sheet_data
|
|
.get_mut(&row)
|
|
.expect("expected a row")
|
|
.get_mut(&column)
|
|
.expect("expected a column") = Cell::CellFormulaBoolean { f, s, v: *value };
|
|
}
|
|
CalcResult::Error {
|
|
error,
|
|
origin,
|
|
message,
|
|
} => {
|
|
let o = match self.cell_reference_to_string(origin) {
|
|
Ok(s) => s,
|
|
Err(_) => "".to_string(),
|
|
};
|
|
*self.workbook.worksheets[sheet as usize]
|
|
.sheet_data
|
|
.get_mut(&row)
|
|
.expect("expected a row")
|
|
.get_mut(&column)
|
|
.expect("expected a column") = Cell::CellFormulaError {
|
|
f,
|
|
s,
|
|
o,
|
|
m: message.to_string(),
|
|
ei: error.clone(),
|
|
};
|
|
}
|
|
CalcResult::Range { left, right } => {
|
|
let range = Range {
|
|
left: *left,
|
|
right: *right,
|
|
};
|
|
if let Some(intersection_cell) = implicit_intersection(&cell_reference, &range)
|
|
{
|
|
let v = self.evaluate_cell(intersection_cell);
|
|
self.set_cell_value(cell_reference, &v);
|
|
} else {
|
|
let o = match self.cell_reference_to_string(&cell_reference) {
|
|
Ok(s) => s,
|
|
Err(_) => "".to_string(),
|
|
};
|
|
*self.workbook.worksheets[sheet as usize]
|
|
.sheet_data
|
|
.get_mut(&row)
|
|
.expect("expected a row")
|
|
.get_mut(&column)
|
|
.expect("expected a column") = Cell::CellFormulaError {
|
|
f,
|
|
s,
|
|
o,
|
|
m: "Invalid reference".to_string(),
|
|
ei: Error::VALUE,
|
|
};
|
|
}
|
|
}
|
|
CalcResult::EmptyCell | CalcResult::EmptyArg => {
|
|
*self.workbook.worksheets[sheet as usize]
|
|
.sheet_data
|
|
.get_mut(&row)
|
|
.expect("expected a row")
|
|
.get_mut(&column)
|
|
.expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sets the color of the sheet tab.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use ironcalc_base::model::Model;
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
/// assert_eq!(model.workbook.worksheet(0)?.color, None);
|
|
/// model.set_sheet_color(0, "#DBBE29")?;
|
|
/// assert_eq!(model.workbook.worksheet(0)?.color, Some("#DBBE29".to_string()));
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn set_sheet_color(&mut self, sheet: u32, color: &str) -> Result<(), String> {
|
|
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
|
if color.is_empty() {
|
|
worksheet.color = None;
|
|
return Ok(());
|
|
}
|
|
if common::is_valid_hex_color(color) {
|
|
worksheet.color = Some(color.to_string());
|
|
return Ok(());
|
|
}
|
|
Err(format!("Invalid color: {}", color))
|
|
}
|
|
|
|
fn get_cell_value(&self, cell: &Cell, cell_reference: CellReferenceIndex) -> CalcResult {
|
|
use Cell::*;
|
|
match cell {
|
|
EmptyCell { .. } => CalcResult::EmptyCell,
|
|
BooleanCell { v, .. } => CalcResult::Boolean(*v),
|
|
NumberCell { v, .. } => CalcResult::Number(*v),
|
|
ErrorCell { ei, .. } => {
|
|
let message = ei.to_localized_error_string(&self.language);
|
|
CalcResult::new_error(ei.clone(), cell_reference, message)
|
|
}
|
|
SharedString { si, .. } => {
|
|
if let Some(s) = self.workbook.shared_strings.get(*si as usize) {
|
|
CalcResult::String(s.clone())
|
|
} else {
|
|
let message = "Invalid shared string".to_string();
|
|
CalcResult::new_error(Error::ERROR, cell_reference, message)
|
|
}
|
|
}
|
|
CellFormula { .. } => CalcResult::Error {
|
|
error: Error::ERROR,
|
|
origin: cell_reference,
|
|
message: "Unevaluated formula".to_string(),
|
|
},
|
|
CellFormulaBoolean { v, .. } => CalcResult::Boolean(*v),
|
|
CellFormulaNumber { v, .. } => CalcResult::Number(*v),
|
|
CellFormulaString { v, .. } => CalcResult::String(v.clone()),
|
|
CellFormulaError { ei, o, m, .. } => {
|
|
if let Some(cell_reference) = self.parse_reference(o) {
|
|
CalcResult::new_error(ei.clone(), cell_reference, m.clone())
|
|
} else {
|
|
CalcResult::Error {
|
|
error: ei.clone(),
|
|
origin: cell_reference,
|
|
message: ei.to_localized_error_string(&self.language),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if the cell is completely empty.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use ironcalc_base::model::Model;
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
/// assert_eq!(model.is_empty_cell(0, 1, 1)?, true);
|
|
/// model.set_user_input(0, 1, 1, "Attention is all you need".to_string());
|
|
/// assert_eq!(model.is_empty_cell(0, 1, 1)?, false);
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn is_empty_cell(&self, sheet: u32, row: i32, column: i32) -> Result<bool, String> {
|
|
self.workbook.worksheet(sheet)?.is_empty_cell(row, column)
|
|
}
|
|
|
|
pub(crate) fn evaluate_cell(&mut self, cell_reference: CellReferenceIndex) -> CalcResult {
|
|
let row_data = match self.workbook.worksheets[cell_reference.sheet as usize]
|
|
.sheet_data
|
|
.get(&cell_reference.row)
|
|
{
|
|
Some(r) => r,
|
|
None => return CalcResult::EmptyCell,
|
|
};
|
|
let cell = match row_data.get(&cell_reference.column) {
|
|
Some(c) => c,
|
|
None => {
|
|
return CalcResult::EmptyCell;
|
|
}
|
|
};
|
|
|
|
match cell.get_formula() {
|
|
Some(f) => {
|
|
let key = (
|
|
cell_reference.sheet,
|
|
cell_reference.row,
|
|
cell_reference.column,
|
|
);
|
|
match self.cells.get(&key) {
|
|
Some(CellState::Evaluating) => {
|
|
return CalcResult::new_error(
|
|
Error::CIRC,
|
|
cell_reference,
|
|
"Circular reference detected".to_string(),
|
|
);
|
|
}
|
|
Some(CellState::Evaluated) => {
|
|
return self.get_cell_value(cell, cell_reference);
|
|
}
|
|
_ => {
|
|
// mark cell as being evaluated
|
|
self.cells.insert(key, CellState::Evaluating);
|
|
}
|
|
}
|
|
let node = &self.parsed_formulas[cell_reference.sheet as usize][f as usize].clone();
|
|
let result = self.evaluate_node_in_context(node, cell_reference);
|
|
self.set_cell_value(cell_reference, &result);
|
|
// mark cell as evaluated
|
|
self.cells.insert(key, CellState::Evaluated);
|
|
result
|
|
}
|
|
None => self.get_cell_value(cell, cell_reference),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn get_sheet_index_by_name(&self, name: &str) -> Option<u32> {
|
|
let worksheets = &self.workbook.worksheets;
|
|
for (index, worksheet) in worksheets.iter().enumerate() {
|
|
if worksheet.get_name().to_uppercase() == name.to_uppercase() {
|
|
return Some(index as u32);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Returns a model from a String representation of a workbook
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use ironcalc_base::model::Model;
|
|
/// # use ironcalc_base::cell::CellValue;
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
/// model.set_user_input(0, 1, 1, "Stella!".to_string());
|
|
/// let model2 = Model::from_json(&model.to_json_str())?;
|
|
/// assert_eq!(
|
|
/// model2.get_cell_value_by_index(0, 1, 1),
|
|
/// Ok(CellValue::String("Stella!".to_string()))
|
|
/// );
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn from_json(s: &str) -> Result<Model, String> {
|
|
let workbook: Workbook =
|
|
serde_json::from_str(s).map_err(|_| "Error parsing workbook".to_string())?;
|
|
Model::from_workbook(workbook)
|
|
}
|
|
|
|
/// Returns a model from a Workbook object
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use ironcalc_base::model::Model;
|
|
/// # use ironcalc_base::cell::CellValue;
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
/// model.set_user_input(0, 1, 1, "Stella!".to_string());
|
|
/// let model2 = Model::from_workbook(model.workbook)?;
|
|
/// assert_eq!(
|
|
/// model2.get_cell_value_by_index(0, 1, 1),
|
|
/// Ok(CellValue::String("Stella!".to_string()))
|
|
/// );
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn from_workbook(workbook: Workbook) -> Result<Model, String> {
|
|
let parsed_formulas = Vec::new();
|
|
let worksheets = &workbook.worksheets;
|
|
|
|
let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect();
|
|
|
|
// add all tables
|
|
// let mut tables = Vec::new();
|
|
// for worksheet in worksheets {
|
|
// let mut tables_in_sheet = HashMap::new();
|
|
// for table in &worksheet.tables {
|
|
// tables_in_sheet.insert(table.name.clone(), table.clone());
|
|
// }
|
|
// tables.push(tables_in_sheet);
|
|
// }
|
|
let parser = Parser::new(worksheet_names, workbook.tables.clone());
|
|
let cells = HashMap::new();
|
|
let locale = get_locale(&workbook.settings.locale)
|
|
.map_err(|_| "Invalid locale".to_string())?
|
|
.clone();
|
|
let tz: Tz = workbook
|
|
.settings
|
|
.tz
|
|
.parse()
|
|
.map_err(|_| format!("Invalid timezone: {}", workbook.settings.tz))?;
|
|
|
|
// FIXME: Add support for display languages
|
|
let language = get_language("en").expect("").clone();
|
|
let mut shared_strings = HashMap::new();
|
|
for (index, s) in workbook.shared_strings.iter().enumerate() {
|
|
shared_strings.insert(s.to_string(), index);
|
|
}
|
|
|
|
let mut model = Model {
|
|
workbook,
|
|
parsed_formulas,
|
|
shared_strings,
|
|
parsed_defined_names: HashMap::new(),
|
|
parser,
|
|
cells,
|
|
language,
|
|
locale,
|
|
tz,
|
|
};
|
|
|
|
model.parse_formulas();
|
|
model.parse_defined_names();
|
|
|
|
Ok(model)
|
|
}
|
|
|
|
/// Parses a reference like "Sheet1!B4" into {0, 2, 4}
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use ironcalc_base::model::Model;
|
|
/// # use ironcalc_base::expressions::types::CellReferenceIndex;
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
/// model.set_user_input(0, 1, 1, "Stella!".to_string());
|
|
/// let reference = model.parse_reference("Sheet1!D40");
|
|
/// assert_eq!(reference, Some(CellReferenceIndex {sheet: 0, row: 40, column: 4}));
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn parse_reference(&self, s: &str) -> Option<CellReferenceIndex> {
|
|
let bytes = s.as_bytes();
|
|
let mut sheet_name = "".to_string();
|
|
let mut column = "".to_string();
|
|
let mut row = "".to_string();
|
|
let mut state = "sheet"; // "sheet", "col", "row"
|
|
for &byte in bytes {
|
|
match state {
|
|
"sheet" => {
|
|
if byte == b'!' {
|
|
state = "col"
|
|
} else {
|
|
sheet_name.push(byte as char);
|
|
}
|
|
}
|
|
"col" => {
|
|
if byte.is_ascii_alphabetic() {
|
|
column.push(byte as char);
|
|
} else {
|
|
state = "row";
|
|
row.push(byte as char);
|
|
}
|
|
}
|
|
_ => {
|
|
row.push(byte as char);
|
|
}
|
|
}
|
|
}
|
|
let sheet = match self.get_sheet_index_by_name(&sheet_name) {
|
|
Some(s) => s,
|
|
None => return None,
|
|
};
|
|
let row = match row.parse::<i32>() {
|
|
Ok(r) => r,
|
|
Err(_) => return None,
|
|
};
|
|
if !(1..=constants::LAST_ROW).contains(&row) {
|
|
return None;
|
|
}
|
|
|
|
let column = match utils::column_to_number(&column) {
|
|
Ok(column) => {
|
|
if is_valid_column_number(column) {
|
|
column
|
|
} else {
|
|
return None;
|
|
}
|
|
}
|
|
Err(_) => return None,
|
|
};
|
|
|
|
Some(CellReferenceIndex { sheet, row, column })
|
|
}
|
|
|
|
/// Moves the formula `value` from `source` (in `area`) to `target`.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use ironcalc_base::model::Model;
|
|
/// # use ironcalc_base::expressions::types::{Area, CellReferenceIndex};
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
/// let source = CellReferenceIndex { sheet: 0, row: 3, column: 1};
|
|
/// let target = CellReferenceIndex { sheet: 0, row: 50, column: 1};
|
|
/// let area = Area { sheet: 0, row: 1, column: 1, width: 5, height: 4};
|
|
/// let result = model.move_cell_value_to_area("=B1", &source, &target, &area)?;
|
|
/// assert_eq!(&result, "=B48");
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// See also:
|
|
/// * [Model::extend_to()]
|
|
/// * [Model::extend_copied_value()]
|
|
pub fn move_cell_value_to_area(
|
|
&mut self,
|
|
value: &str,
|
|
source: &CellReferenceIndex,
|
|
target: &CellReferenceIndex,
|
|
area: &Area,
|
|
) -> Result<String, String> {
|
|
let source_sheet_name = self
|
|
.workbook
|
|
.worksheet(source.sheet)
|
|
.map_err(|e| format!("Could not find source worksheet: {}", e))?
|
|
.get_name();
|
|
if source.sheet != area.sheet {
|
|
return Err("Source and area are in different sheets".to_string());
|
|
}
|
|
if source.row < area.row || source.row >= area.row + area.height {
|
|
return Err("Source is outside the area".to_string());
|
|
}
|
|
if source.column < area.column || source.column >= area.column + area.width {
|
|
return Err("Source is outside the area".to_string());
|
|
}
|
|
let target_sheet_name = self
|
|
.workbook
|
|
.worksheet(target.sheet)
|
|
.map_err(|e| format!("Could not find target worksheet: {}", e))?
|
|
.get_name();
|
|
if let Some(formula) = value.strip_prefix('=') {
|
|
let cell_reference = CellReferenceRC {
|
|
sheet: source_sheet_name.to_owned(),
|
|
row: source.row,
|
|
column: source.column,
|
|
};
|
|
let formula_str = move_formula(
|
|
&self.parser.parse(formula, &Some(cell_reference)),
|
|
&MoveContext {
|
|
source_sheet_name: &source_sheet_name,
|
|
row: source.row,
|
|
column: source.column,
|
|
area,
|
|
target_sheet_name: &target_sheet_name,
|
|
row_delta: target.row - source.row,
|
|
column_delta: target.column - source.column,
|
|
},
|
|
);
|
|
Ok(format!("={}", formula_str))
|
|
} else {
|
|
Ok(value.to_string())
|
|
}
|
|
}
|
|
|
|
/// 'Extends' the value from cell (`sheet`, `row`, `column`) to (`target_row`, `target_column`) in the same sheet
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use ironcalc_base::model::Model;
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
/// let (sheet, row, column) = (0, 1, 1);
|
|
/// model.set_user_input(sheet, row, column, "=B1*D4".to_string());
|
|
/// let (target_row, target_column) = (30, 1);
|
|
/// let result = model.extend_to(sheet, row, column, target_row, target_column)?;
|
|
/// assert_eq!(&result, "=B30*D33");
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// See also:
|
|
/// * [Model::extend_copied_value()]
|
|
/// * [Model::move_cell_value_to_area()]
|
|
pub fn extend_to(
|
|
&self,
|
|
sheet: u32,
|
|
row: i32,
|
|
column: i32,
|
|
target_row: i32,
|
|
target_column: i32,
|
|
) -> Result<String, String> {
|
|
let cell = self.workbook.worksheet(sheet)?.cell(row, column);
|
|
let result = match cell {
|
|
Some(cell) => match cell.get_formula() {
|
|
None => cell.get_text(&self.workbook.shared_strings, &self.language),
|
|
Some(i) => {
|
|
let formula = &self.parsed_formulas[sheet as usize][i as usize];
|
|
let cell_ref = CellReferenceRC {
|
|
sheet: self.workbook.worksheets[sheet as usize].get_name(),
|
|
row: target_row,
|
|
column: target_column,
|
|
};
|
|
format!("={}", to_string(formula, &cell_ref))
|
|
}
|
|
},
|
|
None => "".to_string(),
|
|
};
|
|
Ok(result)
|
|
}
|
|
|
|
/// 'Extends' the formula `value` from `source` to `target`
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use ironcalc_base::model::Model;
|
|
/// # use ironcalc_base::expressions::types::CellReferenceIndex;
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
/// let source = CellReferenceIndex {sheet: 0, row: 1, column: 1};
|
|
/// let target = CellReferenceIndex {sheet: 0, row: 30, column: 1};
|
|
/// let result = model.extend_copied_value("=B1*D4", &source, &target)?;
|
|
/// assert_eq!(&result, "=B30*D33");
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// See also:
|
|
/// * [Model::extend_to()]
|
|
/// * [Model::move_cell_value_to_area()]
|
|
pub fn extend_copied_value(
|
|
&mut self,
|
|
value: &str,
|
|
source: &CellReferenceIndex,
|
|
target: &CellReferenceIndex,
|
|
) -> Result<String, String> {
|
|
let source_sheet_name = match self.workbook.worksheets.get(source.sheet as usize) {
|
|
Some(ws) => ws.get_name(),
|
|
None => {
|
|
return Err("Invalid worksheet index".to_owned());
|
|
}
|
|
};
|
|
let target_sheet_name = match self.workbook.worksheets.get(target.sheet as usize) {
|
|
Some(ws) => ws.get_name(),
|
|
None => {
|
|
return Err("Invalid worksheet index".to_owned());
|
|
}
|
|
};
|
|
if let Some(formula_str) = value.strip_prefix('=') {
|
|
let cell_reference = CellReferenceRC {
|
|
sheet: source_sheet_name.to_string(),
|
|
row: source.row,
|
|
column: source.column,
|
|
};
|
|
let formula = &self.parser.parse(formula_str, &Some(cell_reference));
|
|
let cell_reference = CellReferenceRC {
|
|
sheet: target_sheet_name,
|
|
row: target.row,
|
|
column: target.column,
|
|
};
|
|
return Ok(format!("={}", to_string(formula, &cell_reference)));
|
|
};
|
|
Ok(value.to_string())
|
|
}
|
|
|
|
/// Returns the formula in (`sheet`, `row`, `column`) if any
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use ironcalc_base::model::Model;
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
/// let (sheet, row, column) = (0, 1, 1);
|
|
/// model.set_user_input(sheet, row, column, "=SIN(B1*C3)+1".to_string());
|
|
/// model.evaluate();
|
|
/// let result = model.cell_formula(sheet, row, column)?;
|
|
/// assert_eq!(result, Some("=SIN(B1*C3)+1".to_string()));
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// See also:
|
|
/// * [Model::get_cell_content()]
|
|
pub fn cell_formula(
|
|
&self,
|
|
sheet: u32,
|
|
row: i32,
|
|
column: i32,
|
|
) -> Result<Option<String>, String> {
|
|
let worksheet = self.workbook.worksheet(sheet)?;
|
|
Ok(worksheet.cell(row, column).and_then(|cell| {
|
|
cell.get_formula().map(|formula_index| {
|
|
let formula = &self.parsed_formulas[sheet as usize][formula_index as usize];
|
|
let cell_ref = CellReferenceRC {
|
|
sheet: worksheet.get_name(),
|
|
row,
|
|
column,
|
|
};
|
|
format!("={}", to_string(formula, &cell_ref))
|
|
})
|
|
}))
|
|
}
|
|
|
|
/// Updates the value of a cell with some text
|
|
/// It does not change the style unless needs to add "quoting"
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use ironcalc_base::model::Model;
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
/// let (sheet, row, column) = (0, 1, 1);
|
|
/// model.set_user_input(sheet, row, column, "Hello!".to_string());
|
|
/// assert_eq!(model.get_cell_content(sheet, row, column)?, "Hello!".to_string());
|
|
///
|
|
/// model.update_cell_with_text(sheet, row, column, "Goodbye!");
|
|
/// assert_eq!(model.get_cell_content(sheet, row, column)?, "Goodbye!".to_string());
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// See also:
|
|
/// * [Model::set_user_input()]
|
|
/// * [Model::update_cell_with_number()]
|
|
/// * [Model::update_cell_with_bool()]
|
|
/// * [Model::update_cell_with_formula()]
|
|
pub fn update_cell_with_text(&mut self, sheet: u32, row: i32, column: i32, value: &str) {
|
|
let style_index = self.get_cell_style_index(sheet, row, column);
|
|
let new_style_index;
|
|
if common::value_needs_quoting(value, &self.language) {
|
|
new_style_index = self
|
|
.workbook
|
|
.styles
|
|
.get_style_with_quote_prefix(style_index);
|
|
} else if self.workbook.styles.style_is_quote_prefix(style_index) {
|
|
new_style_index = self
|
|
.workbook
|
|
.styles
|
|
.get_style_without_quote_prefix(style_index);
|
|
} else {
|
|
new_style_index = style_index;
|
|
}
|
|
self.set_cell_with_string(sheet, row, column, value, new_style_index);
|
|
}
|
|
|
|
/// Updates the value of a cell with a boolean value
|
|
/// It does not change the style
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use ironcalc_base::model::Model;
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
/// let (sheet, row, column) = (0, 1, 1);
|
|
/// model.set_user_input(sheet, row, column, "TRUE".to_string());
|
|
/// assert_eq!(model.get_cell_content(sheet, row, column)?, "TRUE".to_string());
|
|
///
|
|
/// model.update_cell_with_bool(sheet, row, column, false);
|
|
/// assert_eq!(model.get_cell_content(sheet, row, column)?, "FALSE".to_string());
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// See also:
|
|
/// * [Model::set_user_input()]
|
|
/// * [Model::update_cell_with_number()]
|
|
/// * [Model::update_cell_with_text()]
|
|
/// * [Model::update_cell_with_formula()]
|
|
pub fn update_cell_with_bool(&mut self, sheet: u32, row: i32, column: i32, value: bool) {
|
|
let style_index = self.get_cell_style_index(sheet, row, column);
|
|
let new_style_index = if self.workbook.styles.style_is_quote_prefix(style_index) {
|
|
self.workbook
|
|
.styles
|
|
.get_style_without_quote_prefix(style_index)
|
|
} else {
|
|
style_index
|
|
};
|
|
let worksheet = &mut self.workbook.worksheets[sheet as usize];
|
|
worksheet.set_cell_with_boolean(row, column, value, new_style_index);
|
|
}
|
|
|
|
/// Updates the value of a cell with a number
|
|
/// It does not change the style
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use ironcalc_base::model::Model;
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
/// let (sheet, row, column) = (0, 1, 1);
|
|
/// model.set_user_input(sheet, row, column, "42".to_string());
|
|
/// assert_eq!(model.get_cell_content(sheet, row, column)?, "42".to_string());
|
|
///
|
|
/// model.update_cell_with_number(sheet, row, column, 23.0);
|
|
/// assert_eq!(model.get_cell_content(sheet, row, column)?, "23".to_string());
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// See also:
|
|
/// * [Model::set_user_input()]
|
|
/// * [Model::update_cell_with_text()]
|
|
/// * [Model::update_cell_with_bool()]
|
|
/// * [Model::update_cell_with_formula()]
|
|
pub fn update_cell_with_number(&mut self, sheet: u32, row: i32, column: i32, value: f64) {
|
|
let style_index = self.get_cell_style_index(sheet, row, column);
|
|
let new_style_index = if self.workbook.styles.style_is_quote_prefix(style_index) {
|
|
self.workbook
|
|
.styles
|
|
.get_style_without_quote_prefix(style_index)
|
|
} else {
|
|
style_index
|
|
};
|
|
let worksheet = &mut self.workbook.worksheets[sheet as usize];
|
|
worksheet.set_cell_with_number(row, column, value, new_style_index);
|
|
}
|
|
|
|
/// Updates the formula of given cell
|
|
/// It does not change the style unless needs to add "quoting"
|
|
/// Expects the formula to start with "="
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use ironcalc_base::model::Model;
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
/// let (sheet, row, column) = (0, 1, 1);
|
|
/// model.set_user_input(sheet, row, column, "=A2*2".to_string());
|
|
/// model.evaluate();
|
|
/// assert_eq!(model.get_cell_content(sheet, row, column)?, "=A2*2".to_string());
|
|
///
|
|
/// model.update_cell_with_formula(sheet, row, column, "=A3*2".to_string());
|
|
/// model.evaluate();
|
|
/// assert_eq!(model.get_cell_content(sheet, row, column)?, "=A3*2".to_string());
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// See also:
|
|
/// * [Model::set_user_input()]
|
|
/// * [Model::update_cell_with_number()]
|
|
/// * [Model::update_cell_with_bool()]
|
|
/// * [Model::update_cell_with_text()]
|
|
pub fn update_cell_with_formula(
|
|
&mut self,
|
|
sheet: u32,
|
|
row: i32,
|
|
column: i32,
|
|
formula: String,
|
|
) -> Result<(), String> {
|
|
let mut style_index = self.get_cell_style_index(sheet, row, column);
|
|
if self.workbook.styles.style_is_quote_prefix(style_index) {
|
|
style_index = self
|
|
.workbook
|
|
.styles
|
|
.get_style_without_quote_prefix(style_index);
|
|
}
|
|
let formula = formula
|
|
.strip_prefix('=')
|
|
.ok_or_else(|| format!("\"{formula}\" is not a valid formula"))?;
|
|
self.set_cell_with_formula(sheet, row, column, formula, style_index)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Sets a cell parametrized by (`sheet`, `row`, `column`) with `value`.
|
|
///
|
|
/// This mimics a user entering a value on a cell.
|
|
///
|
|
/// If you enter a currency `$100` it will set as a number and update the style
|
|
/// Note that for currencies/percentage there is only one possible style
|
|
/// The value is always a string, so we need to try to cast it into numbers/booleans/errors
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use ironcalc_base::model::Model;
|
|
/// # use ironcalc_base::cell::CellValue;
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
/// model.set_user_input(0, 1, 1, "100$".to_string());
|
|
/// model.set_user_input(0, 2, 1, "125$".to_string());
|
|
/// model.set_user_input(0, 3, 1, "-10$".to_string());
|
|
/// model.set_user_input(0, 1, 2, "=SUM(A:A)".to_string());
|
|
/// model.evaluate();
|
|
/// assert_eq!(model.get_cell_value_by_index(0, 1, 2), Ok(CellValue::Number(215.0)));
|
|
/// assert_eq!(model.formatted_cell_value(0, 1, 2), Ok("215$".to_string()));
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// See also:
|
|
/// * [Model::update_cell_with_formula()]
|
|
/// * [Model::update_cell_with_number()]
|
|
/// * [Model::update_cell_with_bool()]
|
|
/// * [Model::update_cell_with_text()]
|
|
pub fn set_user_input(&mut self, sheet: u32, row: i32, column: i32, value: String) {
|
|
// If value starts with "'" then we force the style to be quote_prefix
|
|
let style_index = self.get_cell_style_index(sheet, row, column);
|
|
if let Some(new_value) = value.strip_prefix('\'') {
|
|
// First check if it needs quoting
|
|
let new_style = if common::value_needs_quoting(new_value, &self.language) {
|
|
self.workbook
|
|
.styles
|
|
.get_style_with_quote_prefix(style_index)
|
|
} else {
|
|
style_index
|
|
};
|
|
self.set_cell_with_string(sheet, row, column, new_value, new_style);
|
|
} else {
|
|
let mut new_style_index = style_index;
|
|
if self.workbook.styles.style_is_quote_prefix(style_index) {
|
|
new_style_index = self
|
|
.workbook
|
|
.styles
|
|
.get_style_without_quote_prefix(style_index);
|
|
}
|
|
if let Some(formula) = value.strip_prefix('=') {
|
|
let formula_index = self
|
|
.set_cell_with_formula(sheet, row, column, formula, new_style_index)
|
|
.expect("could not set the cell formula");
|
|
// Update the style if needed
|
|
let cell = CellReferenceIndex { sheet, row, column };
|
|
let parsed_formula = &self.parsed_formulas[sheet as usize][formula_index as usize];
|
|
if let Some(units) = self.compute_node_units(parsed_formula, &cell) {
|
|
let new_style_index = self
|
|
.workbook
|
|
.styles
|
|
.get_style_with_format(new_style_index, &units.get_num_fmt());
|
|
let style = self.workbook.styles.get_style(new_style_index);
|
|
self.set_cell_style(sheet, row, column, &style)
|
|
.expect("Failed setting the style");
|
|
}
|
|
} else {
|
|
let worksheets = &mut self.workbook.worksheets;
|
|
let worksheet = &mut worksheets[sheet as usize];
|
|
|
|
// The list of currencies is '$', '€' and the local currency
|
|
let mut currencies = vec!["$", "€"];
|
|
let currency = &self.locale.currency.symbol;
|
|
if !currencies.iter().any(|e| e == currency) {
|
|
currencies.push(currency);
|
|
}
|
|
// We try to parse as number
|
|
if let Ok((v, number_format)) = parse_formatted_number(&value, ¤cies) {
|
|
if let Some(num_fmt) = number_format {
|
|
// Should not apply the format in the following cases:
|
|
// - we assign a date to already date-formatted cell
|
|
let should_apply_format = !(is_likely_date_number_format(
|
|
&self.workbook.styles.get_style(new_style_index).num_fmt,
|
|
) && is_likely_date_number_format(&num_fmt));
|
|
if should_apply_format {
|
|
new_style_index = self
|
|
.workbook
|
|
.styles
|
|
.get_style_with_format(new_style_index, &num_fmt);
|
|
}
|
|
}
|
|
worksheet.set_cell_with_number(row, column, v, new_style_index);
|
|
return;
|
|
}
|
|
// We try to parse as boolean
|
|
if let Ok(v) = value.to_lowercase().parse::<bool>() {
|
|
worksheet.set_cell_with_boolean(row, column, v, new_style_index);
|
|
return;
|
|
}
|
|
// Check is it is error value
|
|
let upper = value.to_uppercase();
|
|
match get_error_by_name(&upper, &self.language) {
|
|
Some(error) => {
|
|
worksheet.set_cell_with_error(row, column, error, new_style_index);
|
|
}
|
|
None => {
|
|
self.set_cell_with_string(sheet, row, column, &value, new_style_index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn set_cell_with_formula(
|
|
&mut self,
|
|
sheet: u32,
|
|
row: i32,
|
|
column: i32,
|
|
formula: &str,
|
|
style: i32,
|
|
) -> Result<i32, String> {
|
|
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
|
let cell_reference = CellReferenceRC {
|
|
sheet: worksheet.get_name(),
|
|
row,
|
|
column,
|
|
};
|
|
let shared_formulas = &mut worksheet.shared_formulas;
|
|
let mut parsed_formula = self.parser.parse(formula, &Some(cell_reference.clone()));
|
|
// If the formula fails to parse try adding a parenthesis
|
|
// SUM(A1:A3 => SUM(A1:A3)
|
|
if let Node::ParseErrorKind { .. } = parsed_formula {
|
|
let new_parsed_formula = self
|
|
.parser
|
|
.parse(&format!("{})", formula), &Some(cell_reference));
|
|
match new_parsed_formula {
|
|
Node::ParseErrorKind { .. } => {}
|
|
_ => parsed_formula = new_parsed_formula,
|
|
}
|
|
}
|
|
|
|
let s = to_rc_format(&parsed_formula);
|
|
let mut formula_index: i32 = -1;
|
|
if let Some(index) = shared_formulas.iter().position(|x| x == &s) {
|
|
formula_index = index as i32;
|
|
}
|
|
if formula_index == -1 {
|
|
shared_formulas.push(s);
|
|
self.parsed_formulas[sheet as usize].push(parsed_formula);
|
|
formula_index = (shared_formulas.len() as i32) - 1;
|
|
}
|
|
worksheet.set_cell_with_formula(row, column, formula_index, style);
|
|
Ok(formula_index)
|
|
}
|
|
|
|
fn set_cell_with_string(&mut self, sheet: u32, row: i32, column: i32, value: &str, style: i32) {
|
|
let worksheets = &mut self.workbook.worksheets;
|
|
let worksheet = &mut worksheets[sheet as usize];
|
|
match self.shared_strings.get(value) {
|
|
Some(string_index) => {
|
|
worksheet.set_cell_with_string(row, column, *string_index as i32, style);
|
|
}
|
|
None => {
|
|
let string_index = self.workbook.shared_strings.len();
|
|
self.workbook.shared_strings.push(value.to_string());
|
|
self.shared_strings.insert(value.to_string(), string_index);
|
|
worksheet.set_cell_with_string(row, column, string_index as i32, style);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Gets the Excel Value (Bool, Number, String) of a cell
|
|
///
|
|
/// See also:
|
|
/// * [Model::get_cell_value_by_index()]
|
|
pub fn get_cell_value_by_ref(&self, cell_ref: &str) -> Result<CellValue, String> {
|
|
let cell_reference = match self.parse_reference(cell_ref) {
|
|
Some(c) => c,
|
|
None => return Err(format!("Error parsing reference: '{cell_ref}'")),
|
|
};
|
|
let sheet_index = cell_reference.sheet;
|
|
let column = cell_reference.column;
|
|
let row = cell_reference.row;
|
|
|
|
self.get_cell_value_by_index(sheet_index, row, column)
|
|
}
|
|
|
|
/// Returns the cell value for (`sheet`, `row`, `column`)
|
|
///
|
|
/// See also:
|
|
/// * [Model::formatted_cell_value()]
|
|
pub fn get_cell_value_by_index(
|
|
&self,
|
|
sheet_index: u32,
|
|
row: i32,
|
|
column: i32,
|
|
) -> Result<CellValue, String> {
|
|
let cell = self
|
|
.workbook
|
|
.worksheet(sheet_index)?
|
|
.cell(row, column)
|
|
.cloned()
|
|
.unwrap_or_default();
|
|
let cell_value = cell.value(&self.workbook.shared_strings, &self.language);
|
|
Ok(cell_value)
|
|
}
|
|
|
|
/// Returns the formatted cell value for (`sheet`, `row`, `column`)
|
|
///
|
|
/// See also:
|
|
/// * [Model::get_cell_value_by_index()]
|
|
/// * [Model::get_cell_value_by_ref]
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # use ironcalc_base::model::Model;
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
/// let (sheet, row, column) = (0, 1, 1);
|
|
/// model.set_user_input(sheet, row, column, "=1/3".to_string());
|
|
/// model.evaluate();
|
|
/// let result = model.formatted_cell_value(sheet, row, column)?;
|
|
/// assert_eq!(result, "0.333333333".to_string());
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn formatted_cell_value(
|
|
&self,
|
|
sheet_index: u32,
|
|
row: i32,
|
|
column: i32,
|
|
) -> Result<String, String> {
|
|
let format = self.get_style_for_cell(sheet_index, row, column).num_fmt;
|
|
let cell = self
|
|
.workbook
|
|
.worksheet(sheet_index)?
|
|
.cell(row, column)
|
|
.cloned()
|
|
.unwrap_or_default();
|
|
let formatted_value =
|
|
cell.formatted_value(&self.workbook.shared_strings, &self.language, |value| {
|
|
format_number(value, &format, &self.locale).text
|
|
});
|
|
Ok(formatted_value)
|
|
}
|
|
|
|
/// Returns a string with the cell content. If there is a formula returns the formula
|
|
/// If the cell is empty returns the empty string
|
|
/// Raises an error if there is no worksheet
|
|
pub fn get_cell_content(&self, sheet: u32, row: i32, column: i32) -> Result<String, String> {
|
|
let worksheet = self.workbook.worksheet(sheet)?;
|
|
let cell = match worksheet.cell(row, column) {
|
|
Some(c) => c,
|
|
None => return Ok("".to_string()),
|
|
};
|
|
match cell.get_formula() {
|
|
Some(formula_index) => {
|
|
let formula = &self.parsed_formulas[sheet as usize][formula_index as usize];
|
|
let cell_ref = CellReferenceRC {
|
|
sheet: worksheet.get_name(),
|
|
row,
|
|
column,
|
|
};
|
|
Ok(format!("={}", to_string(formula, &cell_ref)))
|
|
}
|
|
None => Ok(cell.get_text(&self.workbook.shared_strings, &self.language)),
|
|
}
|
|
}
|
|
|
|
/// Returns a list of all cells
|
|
pub fn get_all_cells(&self) -> Vec<CellIndex> {
|
|
let mut cells = Vec::new();
|
|
for (index, sheet) in self.workbook.worksheets.iter().enumerate() {
|
|
let mut sorted_rows: Vec<_> = sheet.sheet_data.keys().collect();
|
|
sorted_rows.sort_unstable();
|
|
for row in sorted_rows {
|
|
let row_data = &sheet.sheet_data[row];
|
|
let mut sorted_columns: Vec<_> = row_data.keys().collect();
|
|
sorted_columns.sort_unstable();
|
|
for column in sorted_columns {
|
|
cells.push(CellIndex {
|
|
index: index as u32,
|
|
row: *row,
|
|
column: *column,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
cells
|
|
}
|
|
|
|
/// Evaluates the model with a top-down recursive algorithm
|
|
pub fn evaluate(&mut self) {
|
|
// clear all computation artifacts
|
|
self.cells.clear();
|
|
|
|
let cells = self.get_all_cells();
|
|
|
|
for cell in cells {
|
|
self.evaluate_cell(CellReferenceIndex {
|
|
sheet: cell.index,
|
|
row: cell.row,
|
|
column: cell.column,
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Sets cell to empty. Can be used to delete value without affecting style.
|
|
pub fn set_cell_empty(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
|
|
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
|
worksheet.set_cell_empty(row, column);
|
|
Ok(())
|
|
}
|
|
|
|
/// Deletes a cell by removing it from worksheet data.
|
|
pub fn delete_cell(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
|
|
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
|
|
|
let sheet_data = &mut worksheet.sheet_data;
|
|
if let Some(row_data) = sheet_data.get_mut(&row) {
|
|
row_data.remove(&column);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns the style index for cell (`sheet`, `row`, `column`)
|
|
pub fn get_cell_style_index(&self, sheet: u32, row: i32, column: i32) -> i32 {
|
|
// First check the cell, then row, the column
|
|
let cell = self
|
|
.workbook
|
|
.worksheet(sheet)
|
|
.expect("Invalid sheet")
|
|
.cell(row, column);
|
|
match cell {
|
|
Some(cell) => cell.get_style(),
|
|
None => {
|
|
let rows = &self.workbook.worksheets[sheet as usize].rows;
|
|
for r in rows {
|
|
if r.r == row {
|
|
if r.custom_format {
|
|
return r.s;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
let cols = &self.workbook.worksheets[sheet as usize].cols;
|
|
for c in cols.iter() {
|
|
let min = c.min;
|
|
let max = c.max;
|
|
if column >= min && column <= max {
|
|
return c.style.unwrap_or(0);
|
|
}
|
|
}
|
|
0
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the style for cell (`sheet`, `row`, `column`)
|
|
pub fn get_style_for_cell(&self, sheet: u32, row: i32, column: i32) -> Style {
|
|
self.workbook
|
|
.styles
|
|
.get_style(self.get_cell_style_index(sheet, row, column))
|
|
}
|
|
|
|
/// Returns a JSON string of the workbook
|
|
pub fn to_json_str(&self) -> String {
|
|
match serde_json::to_string(&self.workbook) {
|
|
Ok(s) => s,
|
|
Err(_) => {
|
|
// TODO, is this branch possible at all?
|
|
json!({"error": "Error stringifying workbook"}).to_string()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// bin
|
|
pub fn to_binary_str(&self) -> Vec<u8> {
|
|
let config = config::standard();
|
|
bincode::encode_to_vec(&self.workbook, config).expect("")
|
|
}
|
|
|
|
/// Returns markup representation of the given `sheet`.
|
|
pub fn sheet_markup(&self, sheet: u32) -> Result<String, String> {
|
|
let worksheet = self.workbook.worksheet(sheet)?;
|
|
let dimension = worksheet.dimension();
|
|
|
|
let mut rows = Vec::new();
|
|
|
|
for row in 1..(dimension.max_row + 1) {
|
|
let mut row_markup: Vec<String> = Vec::new();
|
|
|
|
for column in 1..(dimension.max_column + 1) {
|
|
let mut cell_markup = match self.cell_formula(sheet, row, column)? {
|
|
Some(formula) => formula,
|
|
None => self.formatted_cell_value(sheet, row, column)?,
|
|
};
|
|
let style = self.get_style_for_cell(sheet, row, column);
|
|
if style.font.b {
|
|
cell_markup = format!("**{cell_markup}**")
|
|
}
|
|
row_markup.push(cell_markup);
|
|
}
|
|
|
|
rows.push(row_markup.join("|"));
|
|
}
|
|
|
|
Ok(rows.join("\n"))
|
|
}
|
|
|
|
/// Sets the currency of the model.
|
|
/// Currently we only support `USD`, `EUR`, `GBP` and `JPY`
|
|
/// NB: This is not preserved in the JSON.
|
|
pub fn set_currency(&mut self, iso: &str) -> Result<(), &str> {
|
|
// TODO: Add a full list
|
|
let symbol = if iso == "USD" {
|
|
"$"
|
|
} else if iso == "EUR" {
|
|
"€"
|
|
} else if iso == "GBP" {
|
|
"£"
|
|
} else if iso == "JPY" {
|
|
"¥"
|
|
} else {
|
|
return Err("Unsupported currency");
|
|
};
|
|
self.locale.currency = Currency {
|
|
symbol: symbol.to_string(),
|
|
iso: iso.to_string(),
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns the number of frozen rows in `sheet`
|
|
pub fn get_frozen_rows(&self, sheet: u32) -> Result<i32, String> {
|
|
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
|
|
Ok(worksheet.frozen_rows)
|
|
} else {
|
|
Err("Invalid sheet".to_string())
|
|
}
|
|
}
|
|
|
|
/// Return the number of frozen columns in `sheet`
|
|
pub fn get_frozen_columns(&self, sheet: u32) -> Result<i32, String> {
|
|
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
|
|
Ok(worksheet.frozen_columns)
|
|
} else {
|
|
Err("Invalid sheet".to_string())
|
|
}
|
|
}
|
|
|
|
/// Sets the number of frozen rows to `frozen_rows` in the workbook.
|
|
/// Fails if `frozen`_rows` is either too small (<0) or too large (>LAST_ROW)`
|
|
pub fn set_frozen_rows(&mut self, sheet: u32, frozen_rows: i32) -> Result<(), String> {
|
|
if let Some(worksheet) = self.workbook.worksheets.get_mut(sheet as usize) {
|
|
if frozen_rows < 0 {
|
|
return Err("Frozen rows cannot be negative".to_string());
|
|
}
|
|
if frozen_rows >= LAST_ROW {
|
|
return Err("Too many rows".to_string());
|
|
}
|
|
worksheet.frozen_rows = frozen_rows;
|
|
Ok(())
|
|
} else {
|
|
Err("Invalid sheet".to_string())
|
|
}
|
|
}
|
|
|
|
/// Sets the number of frozen columns to `frozen_column` in the workbook.
|
|
/// Fails if `frozen`_columns` is either too small (<0) or too large (>LAST_COLUMN)`
|
|
pub fn set_frozen_columns(&mut self, sheet: u32, frozen_columns: i32) -> Result<(), String> {
|
|
if let Some(worksheet) = self.workbook.worksheets.get_mut(sheet as usize) {
|
|
if frozen_columns < 0 {
|
|
return Err("Frozen columns cannot be negative".to_string());
|
|
}
|
|
if frozen_columns >= LAST_COLUMN {
|
|
return Err("Too many columns".to_string());
|
|
}
|
|
worksheet.frozen_columns = frozen_columns;
|
|
Ok(())
|
|
} else {
|
|
Err("Invalid sheet".to_string())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::CellReferenceIndex as CellReference;
|
|
use crate::{test::util::new_empty_model, types::Cell};
|
|
|
|
#[test]
|
|
fn test_cell_reference_to_string() {
|
|
let model = new_empty_model();
|
|
let reference = CellReference {
|
|
sheet: 0,
|
|
row: 32,
|
|
column: 16,
|
|
};
|
|
assert_eq!(
|
|
model.cell_reference_to_string(&reference),
|
|
Ok("Sheet1!P32".to_string())
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn test_cell_reference_to_string_invalid_worksheet() {
|
|
let model = new_empty_model();
|
|
let reference = CellReference {
|
|
sheet: 10,
|
|
row: 1,
|
|
column: 1,
|
|
};
|
|
assert_eq!(
|
|
model.cell_reference_to_string(&reference),
|
|
Err("Invalid sheet index".to_string())
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn test_cell_reference_to_string_invalid_column() {
|
|
let model = new_empty_model();
|
|
let reference = CellReference {
|
|
sheet: 0,
|
|
row: 1,
|
|
column: 20_000,
|
|
};
|
|
assert_eq!(
|
|
model.cell_reference_to_string(&reference),
|
|
Err("Invalid column".to_string())
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn test_cell_reference_to_string_invalid_row() {
|
|
let model = new_empty_model();
|
|
let reference = CellReference {
|
|
sheet: 0,
|
|
row: 2_000_000,
|
|
column: 1,
|
|
};
|
|
assert_eq!(
|
|
model.cell_reference_to_string(&reference),
|
|
Err("Invalid row".to_string())
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_cell() {
|
|
let mut model = new_empty_model();
|
|
model._set("A1", "35");
|
|
model._set("A2", "");
|
|
let worksheet = model.workbook.worksheet(0).expect("Invalid sheet");
|
|
|
|
assert_eq!(
|
|
worksheet.cell(1, 1),
|
|
Some(&Cell::NumberCell { v: 35.0, s: 0 })
|
|
);
|
|
|
|
assert_eq!(
|
|
worksheet.cell(2, 1),
|
|
Some(&Cell::SharedString { si: 0, s: 0 })
|
|
);
|
|
assert_eq!(worksheet.cell(3, 1), None)
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_cell_invalid_sheet() {
|
|
let model = new_empty_model();
|
|
assert_eq!(
|
|
model.workbook.worksheet(5),
|
|
Err("Invalid sheet index".to_string()),
|
|
)
|
|
}
|
|
}
|