botserver/src/sheet/formulas.rs
Rodrigo Rodriguez (Pragmatismo) 3e75bbff97 MS Office 100% Compatibility - Phase 1 Implementation
- Add rust_xlsxwriter for Excel export with formatting support
- Add docx-rs for Word document import/export with HTML conversion
- Add PPTX export support with slides, shapes, and text elements
- Refactor sheet module into 7 files (types, formulas, handlers, etc)
- Refactor docs module into 6 files (types, handlers, storage, etc)
- Refactor slides module into 6 files (types, handlers, storage, etc)
- Fix collaboration modules (borrow issues, rand compatibility)
- Add ooxmlsdk dependency for future Office 2021 features
- Fix type mismatches in slides storage
- Update security protection API router type

Features:
- Excel: Read xlsx/xlsm/xls, write xlsx with styles
- Word: Read/write docx with formatting preservation
- PowerPoint: Write pptx with slides, shapes, text
- Real-time collaboration via WebSocket (already working)
- Theme-aware UI with --sentient-* CSS variables
2026-01-11 09:56:15 -03:00

1061 lines
33 KiB
Rust

use crate::sheet::types::{FormulaResult, Worksheet};
use chrono::{Datelike, Local, NaiveDate};
pub fn evaluate_formula(formula: &str, worksheet: &Worksheet) -> FormulaResult {
if !formula.starts_with('=') {
return FormulaResult {
value: formula.to_string(),
error: None,
};
}
let expr = formula[1..].to_uppercase();
let evaluators: Vec<fn(&str, &Worksheet) -> Option<String>> = vec![
evaluate_sum,
evaluate_average,
evaluate_count,
evaluate_counta,
evaluate_countblank,
evaluate_countif,
evaluate_sumif,
evaluate_averageif,
evaluate_max,
evaluate_min,
evaluate_if,
evaluate_iferror,
evaluate_vlookup,
evaluate_hlookup,
evaluate_index_match,
evaluate_concatenate,
evaluate_left,
evaluate_right,
evaluate_mid,
evaluate_len,
evaluate_trim,
evaluate_upper,
evaluate_lower,
evaluate_proper,
evaluate_substitute,
evaluate_round,
evaluate_roundup,
evaluate_rounddown,
evaluate_abs,
evaluate_sqrt,
evaluate_power,
evaluate_mod_formula,
evaluate_and,
evaluate_or,
evaluate_not,
evaluate_today,
evaluate_now,
evaluate_date,
evaluate_year,
evaluate_month,
evaluate_day,
evaluate_datedif,
evaluate_arithmetic,
];
for evaluator in evaluators {
if let Some(result) = evaluator(&expr, worksheet) {
return FormulaResult {
value: result,
error: None,
};
}
}
FormulaResult {
value: "#ERROR!".to_string(),
error: Some("Invalid formula".to_string()),
}
}
fn evaluate_sum(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("SUM(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[4..expr.len() - 1];
let values = get_range_values(inner, worksheet);
let sum: f64 = values.iter().sum();
Some(format_number(sum))
}
fn evaluate_average(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("AVERAGE(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[8..expr.len() - 1];
let values = get_range_values(inner, worksheet);
if values.is_empty() {
return Some("#DIV/0!".to_string());
}
let avg = values.iter().sum::<f64>() / values.len() as f64;
Some(format_number(avg))
}
fn evaluate_count(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("COUNT(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[6..expr.len() - 1];
let values = get_range_values(inner, worksheet);
Some(values.len().to_string())
}
fn evaluate_counta(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("COUNTA(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[7..expr.len() - 1];
let count = get_range_string_values(inner, worksheet)
.iter()
.filter(|v| !v.is_empty())
.count();
Some(count.to_string())
}
fn evaluate_countblank(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("COUNTBLANK(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[11..expr.len() - 1];
let (start, end) = parse_range(inner)?;
let mut count = 0;
for row in start.0..=end.0 {
for col in start.1..=end.1 {
let key = format!("{},{}", row, col);
let is_blank = worksheet
.data
.get(&key)
.and_then(|c| c.value.as_ref())
.map(|v| v.is_empty())
.unwrap_or(true);
if is_blank {
count += 1;
}
}
}
Some(count.to_string())
}
fn evaluate_countif(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("COUNTIF(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[8..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
if parts.len() != 2 {
return None;
}
let range = parts[0].trim();
let criteria = parts[1].trim().trim_matches('"');
let values = get_range_string_values(range, worksheet);
let count = count_matching(&values, criteria);
Some(count.to_string())
}
fn evaluate_sumif(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("SUMIF(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[6..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
if parts.len() < 2 {
return None;
}
let criteria_range = parts[0].trim();
let criteria = parts[1].trim().trim_matches('"');
let sum_range = if parts.len() > 2 {
parts[2].trim()
} else {
criteria_range
};
let criteria_values = get_range_string_values(criteria_range, worksheet);
let sum_values = get_range_values(sum_range, worksheet);
let mut sum = 0.0;
for (i, cv) in criteria_values.iter().enumerate() {
if matches_criteria(cv, criteria) {
if let Some(sv) = sum_values.get(i) {
sum += sv;
}
}
}
Some(format_number(sum))
}
fn evaluate_averageif(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("AVERAGEIF(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[10..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
if parts.len() < 2 {
return None;
}
let criteria_range = parts[0].trim();
let criteria = parts[1].trim().trim_matches('"');
let avg_range = if parts.len() > 2 {
parts[2].trim()
} else {
criteria_range
};
let criteria_values = get_range_string_values(criteria_range, worksheet);
let avg_values = get_range_values(avg_range, worksheet);
let mut sum = 0.0;
let mut count = 0;
for (i, cv) in criteria_values.iter().enumerate() {
if matches_criteria(cv, criteria) {
if let Some(av) = avg_values.get(i) {
sum += av;
count += 1;
}
}
}
if count == 0 {
return Some("#DIV/0!".to_string());
}
Some(format_number(sum / count as f64))
}
fn evaluate_max(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("MAX(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[4..expr.len() - 1];
let values = get_range_values(inner, worksheet);
values
.iter()
.cloned()
.fold(None, |max, v| match max {
None => Some(v),
Some(m) => Some(if v > m { v } else { m }),
})
.map(format_number)
}
fn evaluate_min(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("MIN(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[4..expr.len() - 1];
let values = get_range_values(inner, worksheet);
values
.iter()
.cloned()
.fold(None, |min, v| match min {
None => Some(v),
Some(m) => Some(if v < m { v } else { m }),
})
.map(format_number)
}
fn evaluate_if(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("IF(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[3..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
if parts.len() < 2 {
return None;
}
let condition = parts[0].trim();
let true_value = parts[1].trim().trim_matches('"');
let false_value = if parts.len() > 2 {
parts[2].trim().trim_matches('"')
} else {
"FALSE"
};
if evaluate_condition(condition, worksheet) {
Some(true_value.to_string())
} else {
Some(false_value.to_string())
}
}
fn evaluate_iferror(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("IFERROR(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[8..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
if parts.len() != 2 {
return None;
}
let value_expr = parts[0].trim();
let error_value = parts[1].trim().trim_matches('"');
let result = evaluate_formula(&format!("={}", value_expr), worksheet);
if result.error.is_some() || result.value.starts_with('#') {
Some(error_value.to_string())
} else {
Some(result.value)
}
}
fn evaluate_vlookup(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("VLOOKUP(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[8..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
if parts.len() < 3 {
return None;
}
let lookup_value = parts[0].trim().trim_matches('"');
let table_range = parts[1].trim();
let col_index: usize = parts[2].trim().parse().ok()?;
let (start, end) = parse_range(table_range)?;
for row in start.0..=end.0 {
let key = format!("{},{}", row, start.1);
let cell_value = worksheet
.data
.get(&key)
.and_then(|c| c.value.clone())
.unwrap_or_default();
if cell_value.eq_ignore_ascii_case(lookup_value) {
let result_col = start.1 + col_index as u32 - 1;
if result_col > end.1 {
return Some("#REF!".to_string());
}
let result_key = format!("{},{}", row, result_col);
return Some(
worksheet
.data
.get(&result_key)
.and_then(|c| c.value.clone())
.unwrap_or_default(),
);
}
}
Some("#N/A".to_string())
}
fn evaluate_hlookup(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("HLOOKUP(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[8..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
if parts.len() < 3 {
return None;
}
let lookup_value = parts[0].trim().trim_matches('"');
let table_range = parts[1].trim();
let row_index: usize = parts[2].trim().parse().ok()?;
let (start, end) = parse_range(table_range)?;
for col in start.1..=end.1 {
let key = format!("{},{}", start.0, col);
let cell_value = worksheet
.data
.get(&key)
.and_then(|c| c.value.clone())
.unwrap_or_default();
if cell_value.eq_ignore_ascii_case(lookup_value) {
let result_row = start.0 + row_index as u32 - 1;
if result_row > end.0 {
return Some("#REF!".to_string());
}
let result_key = format!("{},{}", result_row, col);
return Some(
worksheet
.data
.get(&result_key)
.and_then(|c| c.value.clone())
.unwrap_or_default(),
);
}
}
Some("#N/A".to_string())
}
fn evaluate_index_match(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("INDEX(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[6..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
if parts.len() < 2 {
return None;
}
let range = parts[0].trim();
let row_num: u32 = parts[1].trim().parse().ok()?;
let col_num: u32 = if parts.len() > 2 {
parts[2].trim().parse().ok()?
} else {
1
};
let (start, _end) = parse_range(range)?;
let target_row = start.0 + row_num - 1;
let target_col = start.1 + col_num - 1;
let key = format!("{},{}", target_row, target_col);
Some(
worksheet
.data
.get(&key)
.and_then(|c| c.value.clone())
.unwrap_or_default(),
)
}
fn evaluate_concatenate(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("CONCATENATE(") && !expr.starts_with("CONCAT(") {
return None;
}
if !expr.ends_with(')') {
return None;
}
let start_idx = if expr.starts_with("CONCATENATE(") {
12
} else {
7
};
let inner = &expr[start_idx..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
let result: String = parts
.iter()
.map(|p| {
let trimmed = p.trim().trim_matches('"');
resolve_cell_value(trimmed, worksheet)
})
.collect();
Some(result)
}
fn evaluate_left(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("LEFT(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[5..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
let text = resolve_cell_value(parts[0].trim().trim_matches('"'), worksheet);
let num_chars: usize = if parts.len() > 1 {
parts[1].trim().parse().unwrap_or(1)
} else {
1
};
Some(text.chars().take(num_chars).collect())
}
fn evaluate_right(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("RIGHT(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[6..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
let text = resolve_cell_value(parts[0].trim().trim_matches('"'), worksheet);
let num_chars: usize = if parts.len() > 1 {
parts[1].trim().parse().unwrap_or(1)
} else {
1
};
let len = text.chars().count();
let skip = len.saturating_sub(num_chars);
Some(text.chars().skip(skip).collect())
}
fn evaluate_mid(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("MID(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[4..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
if parts.len() < 3 {
return None;
}
let text = resolve_cell_value(parts[0].trim().trim_matches('"'), worksheet);
let start_pos: usize = parts[1].trim().parse().unwrap_or(1);
let num_chars: usize = parts[2].trim().parse().unwrap_or(1);
Some(
text.chars()
.skip(start_pos.saturating_sub(1))
.take(num_chars)
.collect(),
)
}
fn evaluate_len(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("LEN(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[4..expr.len() - 1];
let text = resolve_cell_value(inner.trim().trim_matches('"'), worksheet);
Some(text.chars().count().to_string())
}
fn evaluate_trim(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("TRIM(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[5..expr.len() - 1];
let text = resolve_cell_value(inner.trim().trim_matches('"'), worksheet);
Some(text.split_whitespace().collect::<Vec<_>>().join(" "))
}
fn evaluate_upper(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("UPPER(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[6..expr.len() - 1];
let text = resolve_cell_value(inner.trim().trim_matches('"'), worksheet);
Some(text.to_uppercase())
}
fn evaluate_lower(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("LOWER(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[6..expr.len() - 1];
let text = resolve_cell_value(inner.trim().trim_matches('"'), worksheet);
Some(text.to_lowercase())
}
fn evaluate_proper(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("PROPER(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[7..expr.len() - 1];
let text = resolve_cell_value(inner.trim().trim_matches('"'), worksheet);
let result: String = text
.split_whitespace()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => {
let mut result = first.to_uppercase().to_string();
result.push_str(&chars.as_str().to_lowercase());
result
}
}
})
.collect::<Vec<_>>()
.join(" ");
Some(result)
}
fn evaluate_substitute(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("SUBSTITUTE(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[11..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
if parts.len() < 3 {
return None;
}
let text = resolve_cell_value(parts[0].trim().trim_matches('"'), worksheet);
let old_text = parts[1].trim().trim_matches('"');
let new_text = parts[2].trim().trim_matches('"');
Some(text.replace(old_text, new_text))
}
fn evaluate_round(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("ROUND(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[6..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
let num: f64 = resolve_cell_value(parts[0].trim(), worksheet)
.parse()
.ok()?;
let decimals: i32 = if parts.len() > 1 {
parts[1].trim().parse().unwrap_or(0)
} else {
0
};
let factor = 10_f64.powi(decimals);
Some(format_number((num * factor).round() / factor))
}
fn evaluate_roundup(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("ROUNDUP(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[8..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
let num: f64 = resolve_cell_value(parts[0].trim(), worksheet)
.parse()
.ok()?;
let decimals: i32 = if parts.len() > 1 {
parts[1].trim().parse().unwrap_or(0)
} else {
0
};
let factor = 10_f64.powi(decimals);
Some(format_number((num * factor).ceil() / factor))
}
fn evaluate_rounddown(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("ROUNDDOWN(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[10..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
let num: f64 = resolve_cell_value(parts[0].trim(), worksheet)
.parse()
.ok()?;
let decimals: i32 = if parts.len() > 1 {
parts[1].trim().parse().unwrap_or(0)
} else {
0
};
let factor = 10_f64.powi(decimals);
Some(format_number((num * factor).floor() / factor))
}
fn evaluate_abs(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("ABS(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[4..expr.len() - 1];
let num: f64 = resolve_cell_value(inner.trim(), worksheet).parse().ok()?;
Some(format_number(num.abs()))
}
fn evaluate_sqrt(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("SQRT(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[5..expr.len() - 1];
let num: f64 = resolve_cell_value(inner.trim(), worksheet).parse().ok()?;
if num < 0.0 {
return Some("#NUM!".to_string());
}
Some(format_number(num.sqrt()))
}
fn evaluate_power(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("POWER(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[6..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
if parts.len() != 2 {
return None;
}
let base: f64 = resolve_cell_value(parts[0].trim(), worksheet)
.parse()
.ok()?;
let exp: f64 = resolve_cell_value(parts[1].trim(), worksheet)
.parse()
.ok()?;
Some(format_number(base.powf(exp)))
}
fn evaluate_mod_formula(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("MOD(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[4..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
if parts.len() != 2 {
return None;
}
let number: f64 = resolve_cell_value(parts[0].trim(), worksheet)
.parse()
.ok()?;
let divisor: f64 = resolve_cell_value(parts[1].trim(), worksheet)
.parse()
.ok()?;
if divisor == 0.0 {
return Some("#DIV/0!".to_string());
}
Some(format_number(number % divisor))
}
fn evaluate_and(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("AND(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[4..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
let result = parts
.iter()
.all(|p| evaluate_condition(p.trim(), worksheet));
Some(result.to_string().to_uppercase())
}
fn evaluate_or(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("OR(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[3..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
let result = parts
.iter()
.any(|p| evaluate_condition(p.trim(), worksheet));
Some(result.to_string().to_uppercase())
}
fn evaluate_not(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("NOT(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[4..expr.len() - 1];
let result = !evaluate_condition(inner.trim(), worksheet);
Some(result.to_string().to_uppercase())
}
fn evaluate_today(_expr: &str, _worksheet: &Worksheet) -> Option<String> {
if _expr != "TODAY()" {
return None;
}
let today = Local::now().date_naive();
Some(today.format("%Y-%m-%d").to_string())
}
fn evaluate_now(_expr: &str, _worksheet: &Worksheet) -> Option<String> {
if _expr != "NOW()" {
return None;
}
let now = Local::now();
Some(now.format("%Y-%m-%d %H:%M:%S").to_string())
}
fn evaluate_date(expr: &str, _worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("DATE(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[5..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
if parts.len() != 3 {
return None;
}
let year: i32 = parts[0].trim().parse().ok()?;
let month: u32 = parts[1].trim().parse().ok()?;
let day: u32 = parts[2].trim().parse().ok()?;
let date = NaiveDate::from_ymd_opt(year, month, day)?;
Some(date.format("%Y-%m-%d").to_string())
}
fn evaluate_year(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("YEAR(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[5..expr.len() - 1];
let date_str = resolve_cell_value(inner.trim().trim_matches('"'), worksheet);
let date = NaiveDate::parse_from_str(&date_str, "%Y-%m-%d").ok()?;
Some(date.year().to_string())
}
fn evaluate_month(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("MONTH(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[6..expr.len() - 1];
let date_str = resolve_cell_value(inner.trim().trim_matches('"'), worksheet);
let date = NaiveDate::parse_from_str(&date_str, "%Y-%m-%d").ok()?;
Some(date.month().to_string())
}
fn evaluate_day(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("DAY(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[4..expr.len() - 1];
let date_str = resolve_cell_value(inner.trim().trim_matches('"'), worksheet);
let date = NaiveDate::parse_from_str(&date_str, "%Y-%m-%d").ok()?;
Some(date.day().to_string())
}
fn evaluate_datedif(expr: &str, worksheet: &Worksheet) -> Option<String> {
if !expr.starts_with("DATEDIF(") || !expr.ends_with(')') {
return None;
}
let inner = &expr[8..expr.len() - 1];
let parts: Vec<&str> = split_args(inner);
if parts.len() != 3 {
return None;
}
let start_str = resolve_cell_value(parts[0].trim().trim_matches('"'), worksheet);
let end_str = resolve_cell_value(parts[1].trim().trim_matches('"'), worksheet);
let unit = parts[2].trim().trim_matches('"').to_uppercase();
let start_date = NaiveDate::parse_from_str(&start_str, "%Y-%m-%d").ok()?;
let end_date = NaiveDate::parse_from_str(&end_str, "%Y-%m-%d").ok()?;
let diff = end_date.signed_duration_since(start_date);
let result = match unit.as_str() {
"D" => diff.num_days(),
"M" => {
let months = (end_date.year() - start_date.year()) * 12
+ end_date.month() as i32
- start_date.month() as i32;
i64::from(months)
}
"Y" => i64::from(end_date.year() - start_date.year()),
_ => return Some("#VALUE!".to_string()),
};
Some(result.to_string())
}
fn evaluate_arithmetic(expr: &str, worksheet: &Worksheet) -> Option<String> {
let resolved = resolve_cell_references(expr, worksheet);
eval_simple_arithmetic(&resolved).map(format_number)
}
pub fn resolve_cell_references(expr: &str, worksheet: &Worksheet) -> String {
let mut result = expr.to_string();
let re = regex::Regex::new(r"([A-Z]+)(\d+)").ok();
if let Some(regex) = re {
for cap in regex.captures_iter(expr) {
if let (Some(col_match), Some(row_match)) = (cap.get(1), cap.get(2)) {
let col = col_name_to_index(col_match.as_str());
let row: u32 = row_match.as_str().parse().unwrap_or(1) - 1;
let key = format!("{},{}", row, col);
let value = worksheet
.data
.get(&key)
.and_then(|c| c.value.clone())
.unwrap_or_else(|| "0".to_string());
let cell_ref = format!("{}{}", col_match.as_str(), row_match.as_str());
result = result.replace(&cell_ref, &value);
}
}
}
result
}
fn eval_simple_arithmetic(expr: &str) -> Option<f64> {
let expr = expr.replace(' ', "");
if let Ok(num) = expr.parse::<f64>() {
return Some(num);
}
if let Some(pos) = expr.rfind('+') {
if pos > 0 {
let left = eval_simple_arithmetic(&expr[..pos])?;
let right = eval_simple_arithmetic(&expr[pos + 1..])?;
return Some(left + right);
}
}
if let Some(pos) = expr.rfind('-') {
if pos > 0 {
let left = eval_simple_arithmetic(&expr[..pos])?;
let right = eval_simple_arithmetic(&expr[pos + 1..])?;
return Some(left - right);
}
}
if let Some(pos) = expr.rfind('*') {
let left = eval_simple_arithmetic(&expr[..pos])?;
let right = eval_simple_arithmetic(&expr[pos + 1..])?;
return Some(left * right);
}
if let Some(pos) = expr.rfind('/') {
let left = eval_simple_arithmetic(&expr[..pos])?;
let right = eval_simple_arithmetic(&expr[pos + 1..])?;
if right != 0.0 {
return Some(left / right);
}
}
None
}
pub fn get_range_values(range: &str, worksheet: &Worksheet) -> Vec<f64> {
let parts: Vec<&str> = range.split(':').collect();
if parts.len() != 2 {
if let Ok(val) = resolve_cell_value(range.trim(), worksheet).parse::<f64>() {
return vec![val];
}
return Vec::new();
}
let (start, end) = match parse_range(range) {
Some(r) => r,
None => return Vec::new(),
};
let mut values = Vec::new();
for row in start.0..=end.0 {
for col in start.1..=end.1 {
let key = format!("{},{}", row, col);
if let Some(cell) = worksheet.data.get(&key) {
if let Some(ref value) = cell.value {
if let Ok(num) = value.parse::<f64>() {
values.push(num);
}
}
}
}
}
values
}
pub fn get_range_string_values(range: &str, worksheet: &Worksheet) -> Vec<String> {
let (start, end) = match parse_range(range) {
Some(r) => r,
None => return Vec::new(),
};
let mut values = Vec::new();
for row in start.0..=end.0 {
for col in start.1..=end.1 {
let key = format!("{},{}", row, col);
let value = worksheet
.data
.get(&key)
.and_then(|c| c.value.clone())
.unwrap_or_default();
values.push(value);
}
}
values
}
pub fn parse_range(range: &str) -> Option<((u32, u32), (u32, u32))> {
let parts: Vec<&str> = range.split(':').collect();
if parts.len() != 2 {
return None;
}
let start = parse_cell_ref(parts[0].trim())?;
let end = parse_cell_ref(parts[1].trim())?;
Some((start, end))
}
pub fn parse_cell_ref(cell_ref: &str) -> Option<(u32, u32)> {
let cell_ref = cell_ref.trim().to_uppercase();
let mut col_str = String::new();
let mut row_str = String::new();
for ch in cell_ref.chars() {
if ch.is_ascii_alphabetic() {
col_str.push(ch);
} else if ch.is_ascii_digit() {
row_str.push(ch);
}
}
if col_str.is_empty() || row_str.is_empty() {
return None;
}
let col = col_name_to_index(&col_str);
let row: u32 = row_str.parse::<u32>().ok()? - 1;
Some((row, col))
}
pub fn col_name_to_index(name: &str) -> u32 {
let mut col: u32 = 0;
for ch in name.chars() {
col = col * 26 + (ch as u32 - 'A' as u32 + 1);
}
col - 1
}
pub fn format_number(num: f64) -> String {
if num.fract() == 0.0 {
format!("{}", num as i64)
} else {
format!("{:.6}", num)
.trim_end_matches('0')
.trim_end_matches('.')
.to_string()
}
}
pub fn resolve_cell_value(value: &str, worksheet: &Worksheet) -> String {
if let Some((row, col)) = parse_cell_ref(value) {
let key = format!("{},{}", row, col);
worksheet
.data
.get(&key)
.and_then(|c| c.value.clone())
.unwrap_or_default()
} else {
value.to_string()
}
}
pub fn split_args(s: &str) -> Vec<&str> {
let mut parts = Vec::new();
let mut depth = 0;
let mut start = 0;
for (i, ch) in s.char_indices() {
match ch {
'(' => depth += 1,
')' => depth -= 1,
',' if depth == 0 => {
parts.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
if start < s.len() {
parts.push(&s[start..]);
}
parts
}
fn evaluate_condition(condition: &str, worksheet: &Worksheet) -> bool {
let condition = condition.trim();
if condition.eq_ignore_ascii_case("TRUE") {
return true;
}
if condition.eq_ignore_ascii_case("FALSE") {
return false;
}
let operators = [">=", "<=", "<>", "!=", "=", ">", "<"];
for op in &operators {
if let Some(pos) = condition.find(op) {
let left = resolve_cell_value(condition[..pos].trim(), worksheet);
let right = resolve_cell_value(condition[pos + op.len()..].trim().trim_matches('"'), worksheet);
let left_num = left.parse::<f64>().ok();
let right_num = right.parse::<f64>().ok();
return match (*op, left_num, right_num) {
(">=", Some(l), Some(r)) => l >= r,
("<=", Some(l), Some(r)) => l <= r,
("<>" | "!=", Some(l), Some(r)) => (l - r).abs() > f64::EPSILON,
("<>" | "!=", _, _) => left != right,
("=", Some(l), Some(r)) => (l - r).abs() < f64::EPSILON,
("=", _, _) => left.eq_ignore_ascii_case(&right),
(">", Some(l), Some(r)) => l > r,
("<", Some(l), Some(r)) => l < r,
_ => false,
};
}
}
false
}
fn matches_criteria(value: &str, criteria: &str) -> bool {
if criteria.starts_with(">=") {
if let (Ok(v), Ok(c)) = (value.parse::<f64>(), criteria[2..].parse::<f64>()) {
return v >= c;
}
} else if criteria.starts_with("<=") {
if let (Ok(v), Ok(c)) = (value.parse::<f64>(), criteria[2..].parse::<f64>()) {
return v <= c;
}
} else if criteria.starts_with("<>") || criteria.starts_with("!=") {
let c = &criteria[2..];
return !value.eq_ignore_ascii_case(c);
} else if criteria.starts_with('>') {
if let (Ok(v), Ok(c)) = (value.parse::<f64>(), criteria[1..].parse::<f64>()) {
return v > c;
}
} else if criteria.starts_with('<') {
if let (Ok(v), Ok(c)) = (value.parse::<f64>(), criteria[1..].parse::<f64>()) {
return v < c;
}
} else if criteria.starts_with('=') {
return value.eq_ignore_ascii_case(&criteria[1..]);
} else if criteria.contains('*') || criteria.contains('?') {
let pattern = criteria.replace('*', ".*").replace('?', ".");
if let Ok(re) = regex::Regex::new(&format!("^{}$", pattern)) {
return re.is_match(value);
}
}
value.eq_ignore_ascii_case(criteria)
}
fn count_matching(values: &[String], criteria: &str) -> usize {
values.iter().filter(|v| matches_criteria(v, criteria)).count()
}