use crate::error::{ OfficeError, Result, XlsxError };
use crate::xlsx::cell::{ CellReference, CellValue };
use std::collections::{ HashMap, HashSet };
use std::fmt;
mod formula_calculator;
mod formula_manager;
pub use formula_manager::*;
#[derive(Debug, Clone, PartialEq)]
pub enum FormulaValue {
Number(f64),
Text(String),
Boolean(bool),
Error(FormulaError),
Array(Vec<Vec<FormulaValue>>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum FormulaError {
DivisionByZero,
ValueError,
ReferenceError,
NameError,
NumError,
NotAvailable,
NullError,
SpillError,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FormulaExpression {
Constant(FormulaValue),
CellRef(CellReference),
RangeRef(CellReference, CellReference),
Function {
name: String,
args: Vec<FormulaExpression>,
},
BinaryOp {
op: BinaryOperator,
left: Box<FormulaExpression>,
right: Box<FormulaExpression>,
},
UnaryOp {
op: UnaryOperator,
operand: Box<FormulaExpression>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum BinaryOperator {
Add,
Subtract,
Multiply,
Divide,
Power,
Equal,
NotEqual,
LessThan,
LessThanOrEqual,
GreaterThan,
GreaterThanOrEqual,
Concatenate,
LogicalOr,
LogicalAnd,
}
#[derive(Debug, Clone, PartialEq)]
pub enum UnaryOperator {
Plus,
Minus,
Percent,
Factorial,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
Number(f64),
String(String),
Identifier(String),
CellReference(String),
Operator(String),
LeftParen,
RightParen,
Comma,
Colon,
Semicolon,
Eof,
}
pub struct FormulaParser {
tokens: Vec<Token>,
current: usize,
}
pub struct FormulaCalculator {
cell_provider: Box<dyn CellProvider>,
function_library: FunctionLibrary,
cache: HashMap<String, FormulaValue>,
}
pub trait CellProvider {
fn get_cell_value(&self, reference: &CellReference) -> Result<CellValue>;
fn get_range_values(
&self,
start: &CellReference,
end: &CellReference
) -> Result<Vec<Vec<CellValue>>>;
}
pub struct FunctionLibrary {
functions: HashMap<String, Box<dyn FormulaFunction>>,
}
pub trait FormulaFunction {
fn name(&self) -> &str;
fn min_args(&self) -> usize;
fn max_args(&self) -> Option<usize>;
fn execute(&self, args: &[FormulaValue]) -> Result<FormulaValue>;
}
#[derive(Debug, Clone)]
pub struct FormulaDependency {
pub formula_cell: CellReference,
pub dependent_cells: HashSet<CellReference>,
}
pub struct FormulaManager {
formulas: HashMap<CellReference, FormulaExpression>,
dependencies: HashMap<CellReference, FormulaDependency>,
calculator: FormulaCalculator,
}
impl fmt::Display for FormulaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FormulaError::DivisionByZero => write!(f, "#DIV/0!"),
FormulaError::ValueError => write!(f, "#VALUE!"),
FormulaError::ReferenceError => write!(f, "#REF!"),
FormulaError::NameError => write!(f, "#NAME?"),
FormulaError::NumError => write!(f, "#NUM!"),
FormulaError::NotAvailable => write!(f, "#N/A"),
FormulaError::NullError => write!(f, "#NULL!"),
FormulaError::SpillError => write!(f, "#SPILL!"),
}
}
}
impl FormulaValue {
pub fn as_number(&self) -> Result<f64> {
match self {
FormulaValue::Number(n) => Ok(*n),
FormulaValue::Boolean(b) => Ok(if *b { 1.0 } else { 0.0 }),
FormulaValue::Text(s) =>
s.parse::<f64>().map_err(|_| {
OfficeError::Xlsx(XlsxError::InvalidFormula {
formula: format!("Cannot convert '{}' to number", s),
})
}),
FormulaValue::Error(e) =>
Err(
OfficeError::Xlsx(XlsxError::InvalidFormula {
formula: format!("Formula error: {:?}", e),
})
),
FormulaValue::Array(_) =>
Err(
OfficeError::Xlsx(XlsxError::InvalidFormula {
formula: "Cannot convert array to number".to_string(),
})
),
}
}
pub fn as_text(&self) -> String {
match self {
FormulaValue::Number(n) => n.to_string(),
FormulaValue::Text(s) => s.clone(),
FormulaValue::Boolean(b) => b.to_string().to_uppercase(),
FormulaValue::Error(e) => e.to_string(),
FormulaValue::Array(_) => "#VALUE!".to_string(),
}
}
pub fn as_boolean(&self) -> Result<bool> {
match self {
FormulaValue::Boolean(b) => Ok(*b),
FormulaValue::Number(n) => Ok(*n != 0.0),
FormulaValue::Text(s) =>
match s.to_uppercase().as_str() {
"TRUE" => Ok(true),
"FALSE" => Ok(false),
_ =>
Err(
OfficeError::Xlsx(XlsxError::InvalidFormula {
formula: format!("Cannot convert '{}' to boolean", s),
})
),
}
FormulaValue::Error(e) =>
Err(
OfficeError::Xlsx(XlsxError::InvalidFormula {
formula: format!("Formula error: {:?}", e),
})
),
FormulaValue::Array(_) =>
Err(
OfficeError::Xlsx(XlsxError::InvalidFormula {
formula: "Cannot convert array to boolean".to_string(),
})
),
}
}
pub fn is_error(&self) -> bool {
matches!(self, FormulaValue::Error(_))
}
pub fn is_number(&self) -> bool {
matches!(self, FormulaValue::Number(_))
}
pub fn is_text(&self) -> bool {
matches!(self, FormulaValue::Text(_))
}
pub fn is_boolean(&self) -> bool {
matches!(self, FormulaValue::Boolean(_))
}
}
impl FormulaParser {
pub fn new(formula: &str) -> Result<Self> {
let tokens = Self::tokenize(formula)?;
Ok(Self { tokens, current: 0 })
}
pub fn parse(&mut self) -> Result<FormulaExpression> {
self.parse_expression()
}
fn tokenize(formula: &str) -> Result<Vec<Token>> {
let mut tokens = Vec::new();
let mut chars = formula.chars().peekable();
while let Some(&ch) = chars.peek() {
match ch {
' ' | '\t' | '\n' | '\r' => {
chars.next();
}
'(' => {
tokens.push(Token::LeftParen);
chars.next();
}
')' => {
tokens.push(Token::RightParen);
chars.next();
}
',' => {
tokens.push(Token::Comma);
chars.next();
}
':' => {
tokens.push(Token::Colon);
chars.next();
}
';' => {
tokens.push(Token::Semicolon);
chars.next();
}
'+' | '-' | '*' | '/' | '^' | '=' | '<' | '>' | '&' | '|' | '%' => {
let mut op = String::new();
op.push(chars.next().unwrap());
let valid_double_ops = [
"<=", ">=", "<>", ];
if let Some(&next_ch) = chars.peek() {
let potential_op = format!("{}{}", ch, next_ch);
if valid_double_ops.contains(&potential_op.as_str()) {
op.push(chars.next().unwrap()); }
}
tokens.push(Token::Operator(op));
}
'"' => {
chars.next(); let mut string_val = String::new();
while let Some(ch) = chars.next() {
if ch == '"' {
if chars.peek() == Some(&'"') {
string_val.push('"');
chars.next();
} else {
break;
}
} else {
string_val.push(ch);
}
}
tokens.push(Token::String(string_val));
}
'0'..='9' | '.' => {
let mut number = String::new();
while let Some(&ch) = chars.peek() {
if ch.is_ascii_digit() || ch == '.' {
number.push(chars.next().unwrap());
} else {
break;
}
}
let num_val = number
.parse::<f64>()
.map_err(|_| {
OfficeError::Xlsx(XlsxError::InvalidFormula { formula: number })
})?;
tokens.push(Token::Number(num_val));
}
'A'..='Z' | 'a'..='z' | '$' => {
let mut identifier = String::new();
while let Some(&ch) = chars.peek() {
if ch.is_ascii_alphanumeric() || ch == '$' || ch == '_' {
identifier.push(chars.next().unwrap());
} else {
break;
}
}
if Self::is_cell_reference(&identifier) {
tokens.push(Token::CellReference(identifier));
} else if
identifier.to_uppercase() == "TRUE" ||
identifier.to_uppercase() == "FALSE"
{
tokens.push(Token::Identifier(identifier));
} else {
tokens.push(Token::Identifier(identifier));
}
}
_ => {
return Err(
OfficeError::Xlsx(XlsxError::InvalidFormula {
formula: format!("Unexpected character: {}", ch),
})
);
}
}
}
tokens.push(Token::Eof);
Ok(tokens)
}
fn is_cell_reference(text: &str) -> bool {
let text = text.trim();
if text.is_empty() {
return false;
}
if text.ends_with("$") {
return false;
}
let mut chars = text.chars().peekable();
let mut dollar_count = 0;
if chars.peek() == Some(&'$') {
chars.next();
dollar_count += 1;
}
let mut has_col_letter = false;
while let Some(ch) = chars.peek() {
if ch.is_ascii_uppercase() {
chars.next();
has_col_letter = true;
} else {
break;
}
}
if !has_col_letter {
return false;
}
if chars.peek() == Some(&'$') {
chars.next();
dollar_count += 1;
}
let mut has_row_number = false;
while let Some(ch) = chars.peek() {
if ch.is_ascii_digit() {
chars.next();
has_row_number = true;
} else {
break;
}
}
if !has_row_number {
return false;
}
if chars.peek() == Some(&'$') {
chars.next();
dollar_count += 1;
if chars.peek().is_some() {
return false;
}
}
chars.next().is_none() && dollar_count <= 2
}
fn parse_expression(&mut self) -> Result<FormulaExpression> {
self.parse_logical_or()
}
fn parse_logical_or(&mut self) -> Result<FormulaExpression> {
let mut expr = self.parse_logical_and()?;
while self.match_operator("|") {
let right = self.parse_logical_and()?;
expr = FormulaExpression::BinaryOp {
op: BinaryOperator::LogicalOr,
left: Box::new(expr),
right: Box::new(right),
};
}
Ok(expr)
}
fn parse_logical_and(&mut self) -> Result<FormulaExpression> {
let mut expr = self.parse_equality()?;
while self.match_operator("&") {
let right = self.parse_equality()?;
expr = FormulaExpression::BinaryOp {
op: BinaryOperator::LogicalAnd,
left: Box::new(expr),
right: Box::new(right),
};
}
Ok(expr)
}
fn parse_equality(&mut self) -> Result<FormulaExpression> {
let mut expr = self.parse_comparison()?;
while let Some(op) = self.match_equality_operator() {
let right = self.parse_comparison()?;
expr = FormulaExpression::BinaryOp {
op,
left: Box::new(expr),
right: Box::new(right),
};
}
Ok(expr)
}
fn parse_comparison(&mut self) -> Result<FormulaExpression> {
let mut expr = self.parse_addition()?;
while let Some(op) = self.match_comparison_operator() {
let right = self.parse_addition()?;
expr = FormulaExpression::BinaryOp {
op,
left: Box::new(expr),
right: Box::new(right),
};
}
Ok(expr)
}
fn parse_addition(&mut self) -> Result<FormulaExpression> {
let mut expr = self.parse_multiplication()?;
while let Some(op) = self.match_addition_operator() {
let right = self.parse_multiplication()?;
expr = FormulaExpression::BinaryOp {
op,
left: Box::new(expr),
right: Box::new(right),
};
}
Ok(expr)
}
fn parse_multiplication(&mut self) -> Result<FormulaExpression> {
let mut expr = self.parse_power()?;
while let Some(op) = self.match_multiplication_operator() {
let right = self.parse_power()?;
expr = FormulaExpression::BinaryOp {
op,
left: Box::new(expr),
right: Box::new(right),
};
}
Ok(expr)
}
fn parse_power(&mut self) -> Result<FormulaExpression> {
let mut expr = self.parse_unary()?;
if self.match_operator("%") {
expr = FormulaExpression::UnaryOp {
op: UnaryOperator::Percent,
operand: Box::new(expr),
};
}
if self.match_operator("^") {
let right = self.parse_power()?; expr = FormulaExpression::BinaryOp {
op: BinaryOperator::Power,
left: Box::new(expr),
right: Box::new(right),
};
}
Ok(expr)
}
fn parse_unary(&mut self) -> Result<FormulaExpression> {
if let Some(op) = self.match_unary_operator() {
if matches!(op, UnaryOperator::Plus | UnaryOperator::Minus) {
let operand = self.parse_unary()?;
return Ok(FormulaExpression::UnaryOp {
op,
operand: Box::new(operand),
});
} else {
self.current -= 1;
}
}
self.parse_primary()
}
fn parse_primary(&mut self) -> Result<FormulaExpression> {
match &self.current_token()? {
Token::Number(n) => {
let value = *n;
self.advance();
Ok(FormulaExpression::Constant(FormulaValue::Number(value)))
}
Token::String(s) => {
let value = s.clone();
self.advance();
Ok(FormulaExpression::Constant(FormulaValue::Text(value)))
}
Token::CellReference(ref_str) => {
let cell_ref = CellReference::from_a1(ref_str)?;
self.advance();
if self.match_token(&Token::Colon) {
if let Token::CellReference(end_ref_str) = &self.current_token()? {
let end_ref = CellReference::from_a1(end_ref_str)?;
self.advance();
Ok(FormulaExpression::RangeRef(cell_ref, end_ref))
} else {
Err(
OfficeError::Xlsx(XlsxError::InvalidFormula {
formula: "Expected cell reference after colon".to_string(),
})
)
}
} else {
Ok(FormulaExpression::CellRef(cell_ref))
}
}
Token::Identifier(name) => {
let func_name = name.clone();
self.advance();
if func_name.to_uppercase() == "TRUE" {
return Ok(FormulaExpression::Constant(FormulaValue::Boolean(true)));
} else if func_name.to_uppercase() == "FALSE" {
return Ok(FormulaExpression::Constant(FormulaValue::Boolean(false)));
}
if self.match_token(&Token::LeftParen) {
let mut args = Vec::new();
if !self.check_token(&Token::RightParen) {
loop {
args.push(self.parse_expression()?);
if !self.match_token(&Token::Comma) {
break;
}
}
}
if !self.match_token(&Token::RightParen) {
return Err(
OfficeError::Xlsx(XlsxError::InvalidFormula {
formula: "Expected ')' after function arguments".to_string(),
})
);
}
Ok(FormulaExpression::Function {
name: func_name,
args,
})
} else {
Err(
OfficeError::Xlsx(XlsxError::InvalidFormula {
formula: format!("Unknown identifier: {}", func_name),
})
)
}
}
Token::LeftParen => {
self.advance();
let expr = self.parse_expression()?;
if !self.match_token(&Token::RightParen) {
return Err(
OfficeError::Xlsx(XlsxError::InvalidFormula {
formula: "Expected ')'".to_string(),
})
);
}
Ok(expr)
}
_ =>
Err(
OfficeError::Xlsx(XlsxError::InvalidFormula {
formula: "Unexpected token".to_string(),
})
),
}
}
fn current_token(&self) -> Result<&Token> {
self.tokens.get(self.current).ok_or_else(|| {
OfficeError::Xlsx(XlsxError::InvalidFormula {
formula: "Unexpected end of formula".to_string(),
})
})
}
fn advance(&mut self) {
if self.current < self.tokens.len() {
self.current += 1;
}
}
fn check_token(&self, token: &Token) -> bool {
if let Ok(current) = self.current_token() {
std::mem::discriminant(current) == std::mem::discriminant(token)
} else {
false
}
}
fn match_token(&mut self, token: &Token) -> bool {
if self.check_token(token) {
self.advance();
true
} else {
false
}
}
fn match_operator(&mut self, op: &str) -> bool {
if let Ok(Token::Operator(current_op)) = self.current_token() {
if current_op == op {
self.advance();
return true;
}
}
false
}
fn match_equality_operator(&mut self) -> Option<BinaryOperator> {
if let Ok(Token::Operator(op)) = self.current_token() {
let result = match op.as_str() {
"=" => Some(BinaryOperator::Equal),
"<>" => Some(BinaryOperator::NotEqual),
_ => None,
};
if result.is_some() {
self.advance();
}
result
} else {
None
}
}
fn match_comparison_operator(&mut self) -> Option<BinaryOperator> {
if let Ok(Token::Operator(op)) = self.current_token() {
let result = match op.as_str() {
"<" => Some(BinaryOperator::LessThan),
"<=" => Some(BinaryOperator::LessThanOrEqual),
">" => Some(BinaryOperator::GreaterThan),
">=" => Some(BinaryOperator::GreaterThanOrEqual),
"<>" => Some(BinaryOperator::NotEqual),
_ => None,
};
if result.is_some() {
self.advance();
}
result
} else {
None
}
}
fn match_addition_operator(&mut self) -> Option<BinaryOperator> {
if let Ok(Token::Operator(op)) = self.current_token() {
let result = match op.as_str() {
"+" => Some(BinaryOperator::Add),
"-" => Some(BinaryOperator::Subtract),
_ => None,
};
if result.is_some() {
self.advance();
}
result
} else {
None
}
}
fn match_multiplication_operator(&mut self) -> Option<BinaryOperator> {
if let Ok(Token::Operator(op)) = self.current_token() {
let result = match op.as_str() {
"*" => Some(BinaryOperator::Multiply),
"/" => Some(BinaryOperator::Divide),
_ => None,
};
if result.is_some() {
self.advance();
}
result
} else {
None
}
}
fn match_unary_operator(&mut self) -> Option<UnaryOperator> {
if let Ok(Token::Operator(op)) = self.current_token() {
let result = match op.as_str() {
"+" => Some(UnaryOperator::Plus),
"-" => Some(UnaryOperator::Minus),
"%" => Some(UnaryOperator::Percent),
"!" => Some(UnaryOperator::Factorial),
_ => None,
};
if result.is_some() {
self.advance();
}
result
} else {
None
}
}
}
pub fn parse_formula(formula: &str) -> Result<FormulaExpression> {
let formula_content = if formula.starts_with('=') { &formula[1..] } else { formula };
let mut parser = FormulaParser::new(formula_content)?;
parser.parse()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tokenize_simple() {
let tokens = FormulaParser::tokenize("1+2").unwrap();
assert_eq!(tokens.len(), 4); }
#[test]
fn test_parse_simple_addition() {
let expr = parse_formula("1+2").unwrap();
match expr {
FormulaExpression::BinaryOp { op: BinaryOperator::Add, .. } => {}
_ => panic!("Expected addition expression"),
}
}
#[test]
fn test_parse_cell_reference() {
let expr = parse_formula("A1").unwrap();
match expr {
FormulaExpression::CellRef(_) => {}
_ => panic!("Expected cell reference"),
}
}
#[test]
fn test_parse_function_call() {
let expr = parse_formula("SUM(A1:A10)").unwrap();
match expr {
FormulaExpression::Function { name, args } => {
assert_eq!(name, "SUM");
assert_eq!(args.len(), 1);
}
_ => panic!("Expected function call"),
}
}
#[test]
fn test_formula_value_conversions() {
let num_val = FormulaValue::Number(42.0);
assert_eq!(num_val.as_number().unwrap(), 42.0);
assert_eq!(num_val.as_text(), "42");
let bool_val = FormulaValue::Boolean(true);
assert_eq!(bool_val.as_boolean().unwrap(), true);
assert_eq!(bool_val.as_number().unwrap(), 1.0);
}
#[test]
fn test_logical_or_operator() {
let expr = parse_formula("TRUE|FALSE").unwrap();
match expr {
FormulaExpression::BinaryOp { op: BinaryOperator::LogicalOr, .. } => {}
_ => panic!("Expected logical OR expression"),
}
}
#[test]
fn test_logical_and_operator() {
let expr = parse_formula("TRUE&FALSE").unwrap();
match expr {
FormulaExpression::BinaryOp { op: BinaryOperator::LogicalAnd, .. } => {}
_ => panic!("Expected logical AND expression"),
}
}
#[test]
fn test_boolean_constants() {
let expr = parse_formula("TRUE").unwrap();
match expr {
FormulaExpression::Constant(FormulaValue::Boolean(true)) => {}
_ => panic!("Expected TRUE constant"),
}
let expr = parse_formula("FALSE").unwrap();
match expr {
FormulaExpression::Constant(FormulaValue::Boolean(false)) => {}
_ => panic!("Expected FALSE constant"),
}
}
#[test]
fn test_mixed_logical_operations() {
let expr = parse_formula("TRUE|FALSE&TRUE").unwrap();
match expr {
FormulaExpression::BinaryOp { op: BinaryOperator::LogicalOr, left, right } => {
match *left {
FormulaExpression::Constant(FormulaValue::Boolean(true)) => {}
_ => panic!("Expected TRUE constant"),
}
match *right {
FormulaExpression::BinaryOp { op: BinaryOperator::LogicalAnd, .. } => {}
_ => panic!("Expected logical AND expression"),
}
}
_ => panic!("Expected logical OR expression"),
}
}
#[test]
fn test_case_insensitive_boolean_constants() {
let expr = parse_formula("true").unwrap();
match expr {
FormulaExpression::Constant(FormulaValue::Boolean(true)) => {}
_ => panic!("Expected TRUE constant"),
}
let expr = parse_formula("False").unwrap();
match expr {
FormulaExpression::Constant(FormulaValue::Boolean(false)) => {}
_ => panic!("Expected FALSE constant"),
}
}
#[test]
fn test_is_cell_reference() {
assert!(FormulaParser::is_cell_reference("A1"));
assert!(FormulaParser::is_cell_reference("Z999"));
assert!(FormulaParser::is_cell_reference("AA1"));
assert!(FormulaParser::is_cell_reference("AZ999"));
assert!(FormulaParser::is_cell_reference("$A$1"));
assert!(FormulaParser::is_cell_reference("$Z$999"));
assert!(FormulaParser::is_cell_reference("A$1"));
assert!(FormulaParser::is_cell_reference("$A1"));
assert!(FormulaParser::is_cell_reference("AA$1"));
assert!(FormulaParser::is_cell_reference("$AA1"));
assert!(!FormulaParser::is_cell_reference("A"));
assert!(!FormulaParser::is_cell_reference("1"));
assert!(!FormulaParser::is_cell_reference("$A"));
assert!(!FormulaParser::is_cell_reference("A$"));
assert!(!FormulaParser::is_cell_reference("$$A1"));
assert!(!FormulaParser::is_cell_reference("A1$"));
assert!(!FormulaParser::is_cell_reference("A1$1"));
assert!(!FormulaParser::is_cell_reference("A$$1"));
assert!(!FormulaParser::is_cell_reference("$A$1$"));
assert!(!FormulaParser::is_cell_reference(""));
assert!(!FormulaParser::is_cell_reference("AB CD"));
}
#[test]
fn test_compound_operators() {
let expr = parse_formula("A1<=B1").unwrap();
match expr {
FormulaExpression::BinaryOp { op: BinaryOperator::LessThanOrEqual, .. } => {}
_ => panic!("Expected less than or equal expression"),
}
let expr = parse_formula("A1>=B1").unwrap();
match expr {
FormulaExpression::BinaryOp { op: BinaryOperator::GreaterThanOrEqual, .. } => {}
_ => panic!("Expected greater than or equal expression"),
}
let expr = parse_formula("A1<>B1").unwrap();
match expr {
FormulaExpression::BinaryOp { op: BinaryOperator::NotEqual, .. } => {}
_ => panic!("Expected not equal expression"),
}
}
#[test]
fn test_invalid_operator_combinations() {
assert!(parse_formula("1<=<2").is_err());
assert!(parse_formula("1>=>2").is_err());
assert!(parse_formula("1&&>2").is_err());
assert!(parse_formula("1||<2").is_err());
assert!(parse_formula("TRUE||FALSE").is_err());
assert!(parse_formula("TRUE&&TRUE").is_err());
assert!(parse_formula("TRUE && TRUE").is_err());
}
#[test]
fn test_operator_spacing() {
let expr = parse_formula("A1 <= B1").unwrap();
match expr {
FormulaExpression::BinaryOp { op: BinaryOperator::LessThanOrEqual, .. } => {}
_ => panic!("Expected less than or equal expression"),
}
}
}