use crate::error::{ForgeError, ForgeResult};
use regex::Regex;
use std::collections::HashMap;
pub struct FormulaTranslator {
column_map: HashMap<String, String>,
table_column_maps: HashMap<String, HashMap<String, String>>,
table_row_counts: HashMap<String, usize>,
}
impl FormulaTranslator {
#[must_use]
pub fn new(column_map: HashMap<String, String>) -> Self {
Self {
column_map,
table_column_maps: HashMap::new(),
table_row_counts: HashMap::new(),
}
}
#[must_use]
pub const fn new_with_tables(
column_map: HashMap<String, String>,
table_column_maps: HashMap<String, HashMap<String, String>>,
table_row_counts: HashMap<String, usize>,
) -> Self {
Self {
column_map,
table_column_maps,
table_row_counts,
}
}
#[allow(clippy::too_many_lines)] pub fn translate_row_formula(&self, formula: &str, excel_row: u32) -> ForgeResult<String> {
let formula_body = formula.strip_prefix('=').unwrap_or(formula);
let mut result = formula_body.to_string();
let agg_pattern = Regex::new(r"(SUM|AVERAGE|MAX|MIN|COUNT|COUNTA|PRODUCT)\(([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\)")
.map_err(|e| ForgeError::Export(format!("Regex error: {e}")))?;
let agg_replacements: Vec<(std::ops::Range<usize>, String)> = agg_pattern
.captures_iter(&result.clone())
.map(|cap| {
let full_match = cap.get(0).unwrap();
let func_name = &cap[1];
let table_name = &cap[2];
let col_name = &cap[3];
let col_letter = self
.table_column_maps
.get(table_name)
.and_then(|cols| cols.get(col_name))
.cloned()
.unwrap_or_else(|| col_name.to_string());
let row_count = self.table_row_counts.get(table_name).copied().unwrap_or(1);
let end_row = row_count + 1;
let replacement =
format!("{func_name}('{table_name}'!{col_letter}2:{col_letter}{end_row})");
(full_match.range(), replacement)
})
.collect();
for (range, replacement) in agg_replacements.into_iter().rev() {
result.replace_range(range, &replacement);
}
let general_table_pattern =
Regex::new(r"\b([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\b")
.map_err(|e| ForgeError::Export(format!("Regex error: {e}")))?;
let general_replacements: Vec<(std::ops::Range<usize>, String)> = general_table_pattern
.captures_iter(&result.clone())
.filter_map(|cap| {
let full_match = cap.get(0).unwrap();
let table_name = &cap[1];
let col_name = &cap[2];
if result[..full_match.start()].ends_with('\'')
|| result[full_match.end()..].starts_with('!')
{
return None;
}
if !self.table_column_maps.contains_key(table_name) {
return None;
}
let col_letter = self
.table_column_maps
.get(table_name)
.and_then(|cols| cols.get(col_name))
.cloned()
.unwrap_or_else(|| col_name.to_string());
let row_count = self.table_row_counts.get(table_name).copied().unwrap_or(1);
let end_row = row_count + 1;
let replacement = format!("'{table_name}'!{col_letter}2:{col_letter}{end_row}");
Some((full_match.range(), replacement))
})
.collect();
for (range, replacement) in general_replacements.into_iter().rev() {
result.replace_range(range, &replacement);
}
let remaining_table_pattern =
Regex::new(r"([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)")
.map_err(|e| ForgeError::Export(format!("Regex error: {e}")))?;
let remaining_replacements: Vec<(std::ops::Range<usize>, String)> = remaining_table_pattern
.captures_iter(&result.clone())
.filter_map(|cap| {
let full_match = cap.get(0).unwrap();
let table_name = &cap[1];
let col_name = &cap[2];
if result[..full_match.start()].ends_with('\'')
|| result[full_match.end()..].starts_with('!')
{
return None;
}
let replacement = format!("'{table_name}'!{col_name}{excel_row}");
Some((full_match.range(), replacement))
})
.collect();
for (range, replacement) in remaining_replacements.into_iter().rev() {
result.replace_range(range, &replacement);
}
let var_pattern = Regex::new(r"\b([a-zA-Z_][a-zA-Z0-9_]*)\b")
.map_err(|e| ForgeError::Export(format!("Regex error: {e}")))?;
let result_clone = result.clone();
let matches: Vec<_> = var_pattern.find_iter(&result_clone).collect();
for match_obj in matches.iter().rev() {
let var_name = match_obj.as_str();
if Self::is_excel_function(var_name) {
continue;
}
let start = match_obj.start();
let end = match_obj.end();
if start > 0 && result[..start].ends_with('!') {
continue;
}
if start > 0 && result[..start].ends_with('\'') {
continue;
}
if end < result.len() && result[end..].starts_with('!') {
continue;
}
if let Some(col_letter) = self.column_map.get(var_name) {
let excel_ref = format!("{col_letter}{excel_row}");
result.replace_range(match_obj.range(), &excel_ref);
} else {
let looks_like_cell_ref = var_name.len() >= 2
&& var_name.chars().all(char::is_alphanumeric)
&& var_name.chars().any(char::is_alphabetic)
&& var_name.chars().any(char::is_numeric);
let is_number = var_name.parse::<f64>().is_ok();
if !looks_like_cell_ref && !is_number {
return Err(ForgeError::Export(format!(
"Column '{var_name}' not found in table"
)));
}
}
}
Ok(format!("={result}"))
}
#[allow(clippy::too_many_lines)] fn is_excel_function(word: &str) -> bool {
let upper = word.to_uppercase();
matches!(
upper.as_str(),
"SUM"
| "AVERAGE"
| "MAX"
| "MIN"
| "COUNT"
| "COUNTA"
| "PRODUCT"
| "MEDIAN"
| "SUMIF"
| "SUMIFS"
| "COUNTIF"
| "COUNTIFS"
| "AVERAGEIF"
| "AVERAGEIFS"
| "MAXIFS"
| "MINIFS"
| "IF"
| "AND"
| "OR"
| "NOT"
| "XOR"
| "TRUE"
| "FALSE"
| "IFERROR"
| "IFNA"
| "CHOOSE"
| "ABS"
| "ROUND"
| "ROUNDUP"
| "ROUNDDOWN"
| "SQRT"
| "POW"
| "POWER"
| "EXP"
| "LN"
| "LOG"
| "LOG10"
| "PI"
| "E"
| "MOD"
| "CEILING"
| "FLOOR"
| "INT"
| "TRUNC"
| "SIGN"
| "SIN"
| "COS"
| "TAN"
| "ASIN"
| "ACOS"
| "ATAN"
| "SINH"
| "COSH"
| "TANH"
| "RADIANS"
| "DEGREES"
| "CONCATENATE"
| "CONCAT"
| "LEFT"
| "RIGHT"
| "MID"
| "LEN"
| "UPPER"
| "LOWER"
| "TRIM"
| "TEXT"
| "VALUE"
| "FIND"
| "SEARCH"
| "REPLACE"
| "SUBSTITUTE"
| "TODAY"
| "NOW"
| "DATE"
| "YEAR"
| "MONTH"
| "DAY"
| "WEEKDAY"
| "HOUR"
| "MINUTE"
| "SECOND"
| "DATEDIF"
| "EDATE"
| "EOMONTH"
| "NETWORKDAYS"
| "WORKDAY"
| "YEARFRAC"
| "DAYS"
| "TIME"
| "NPV"
| "IRR"
| "MIRR"
| "XNPV"
| "XIRR"
| "PMT"
| "FV"
| "PV"
| "RATE"
| "NPER"
| "SLN"
| "DB"
| "DDB"
| "ISEVEN"
| "ISODD"
| "ISBLANK"
| "ISERROR"
| "ISNA"
| "ISNUMBER"
| "ISTEXT"
| "ISLOGICAL"
| "ISREF"
| "ISFORMULA"
| "NA"
| "TYPE"
| "N"
| "VLOOKUP"
| "HLOOKUP"
| "XLOOKUP"
| "INDEX"
| "MATCH"
| "OFFSET"
| "INDIRECT"
| "ADDRESS"
| "ROW"
| "COLUMN"
| "ROWS"
| "COLUMNS"
| "UNIQUE"
| "COUNTUNIQUE"
| "SORT"
| "FILTER"
| "SEQUENCE"
| "RANDARRAY"
| "VAR"
| "VARP"
| "STDEV"
| "STDEVP"
| "CORREL"
| "PERCENTILE"
| "QUARTILE"
| "LARGE"
| "SMALL"
| "RANK"
| "LET"
| "LAMBDA"
| "SWITCH"
| "IFS"
)
}
#[allow(clippy::too_many_lines)] pub fn translate_scalar_formula(
&self,
formula: &str,
scalar_row_map: &HashMap<String, u32>,
) -> ForgeResult<String> {
let formula_body = formula.strip_prefix('=').unwrap_or(formula);
let mut result = formula_body.to_string();
let indexed_pattern =
Regex::new(r"([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\[(\d+)\]")
.map_err(|e| ForgeError::Export(format!("Regex error: {e}")))?;
let indexed_replacements: Vec<(std::ops::Range<usize>, String)> = indexed_pattern
.captures_iter(&result.clone())
.map(|cap| {
let full_match = cap.get(0).unwrap();
let table_name = &cap[1];
let col_name = &cap[2];
let index: usize = cap[3].parse().unwrap_or(0);
let col_letter = self
.table_column_maps
.get(table_name)
.and_then(|cols| cols.get(col_name))
.cloned()
.unwrap_or_else(|| col_name.to_string());
let excel_row = index + 2;
let replacement = format!("'{table_name}'!{col_letter}{excel_row}");
(full_match.range(), replacement)
})
.collect();
for (range, replacement) in indexed_replacements.into_iter().rev() {
result.replace_range(range, &replacement);
}
let agg_pattern = Regex::new(r"(SUM|AVERAGE|MAX|MIN|COUNT|COUNTA|PRODUCT)\(([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\)")
.map_err(|e| ForgeError::Export(format!("Regex error: {e}")))?;
let agg_replacements: Vec<(std::ops::Range<usize>, String)> = agg_pattern
.captures_iter(&result.clone())
.map(|cap| {
let full_match = cap.get(0).unwrap();
let func_name = &cap[1];
let table_name = &cap[2];
let col_name = &cap[3];
let col_letter = self
.table_column_maps
.get(table_name)
.and_then(|cols| cols.get(col_name))
.cloned()
.unwrap_or_else(|| col_name.to_string());
let row_count = self.table_row_counts.get(table_name).copied().unwrap_or(1);
let end_row = row_count + 1;
let replacement =
format!("{func_name}('{table_name}'!{col_letter}2:{col_letter}{end_row})");
(full_match.range(), replacement)
})
.collect();
for (range, replacement) in agg_replacements.into_iter().rev() {
result.replace_range(range, &replacement);
}
let general_table_pattern =
Regex::new(r"\b([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\b")
.map_err(|e| ForgeError::Export(format!("Regex error: {e}")))?;
let general_replacements: Vec<(std::ops::Range<usize>, String)> = general_table_pattern
.captures_iter(&result.clone())
.filter_map(|cap| {
let full_match = cap.get(0).unwrap();
let table_name = &cap[1];
let col_name = &cap[2];
if result[..full_match.start()].ends_with('\'')
|| result[full_match.end()..].starts_with('!')
{
return None;
}
if !self.table_column_maps.contains_key(table_name) {
return None;
}
let col_letter = self
.table_column_maps
.get(table_name)
.and_then(|cols| cols.get(col_name))
.cloned()
.unwrap_or_else(|| col_name.to_string());
let row_count = self.table_row_counts.get(table_name).copied().unwrap_or(1);
let end_row = row_count + 1;
let replacement = format!("'{table_name}'!{col_letter}2:{col_letter}{end_row}");
Some((full_match.range(), replacement))
})
.collect();
for (range, replacement) in general_replacements.into_iter().rev() {
result.replace_range(range, &replacement);
}
let simple_table_pattern =
Regex::new(r"([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)")
.map_err(|e| ForgeError::Export(format!("Regex error: {e}")))?;
let simple_replacements: Vec<(std::ops::Range<usize>, String)> = simple_table_pattern
.captures_iter(&result.clone())
.filter_map(|cap| {
let full_match = cap.get(0).unwrap();
let table_name = &cap[1];
let col_name = &cap[2];
if result[full_match.range()].contains('!') {
return None;
}
if !self.table_column_maps.contains_key(table_name) {
let scalar_name = format!("{table_name}.{col_name}");
if let Some(&row) = scalar_row_map.get(&scalar_name) {
return Some((full_match.range(), format!("B{row}")));
}
return None;
}
let col_letter = self
.table_column_maps
.get(table_name)
.and_then(|cols| cols.get(col_name))
.cloned()
.unwrap_or_else(|| col_name.to_string());
let replacement = format!("'{table_name}'!{col_letter}2");
Some((full_match.range(), replacement))
})
.collect();
for (range, replacement) in simple_replacements.into_iter().rev() {
result.replace_range(range, &replacement);
}
let scalar_pattern = Regex::new(r"\b([a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*)\b")
.map_err(|e| ForgeError::Export(format!("Regex error: {e}")))?;
let scalar_replacements: Vec<(std::ops::Range<usize>, String)> = scalar_pattern
.captures_iter(&result.clone())
.filter_map(|cap| {
let full_match = cap.get(0).unwrap();
let scalar_name = &cap[1];
if result[full_match.range()].contains('!') {
return None;
}
if let Some(&row) = scalar_row_map.get(scalar_name) {
return Some((full_match.range(), format!("B{row}")));
}
None
})
.collect();
for (range, replacement) in scalar_replacements.into_iter().rev() {
result.replace_range(range, &replacement);
}
Ok(format!("={result}"))
}
#[must_use]
pub fn column_index_to_letter(index: usize) -> String {
let mut result = String::new();
let mut idx = index;
loop {
let remainder = idx % 26;
#[allow(clippy::cast_possible_truncation)] let ch = (b'A' + remainder as u8) as char;
result.insert(0, ch);
if idx < 26 {
break;
}
idx = idx / 26 - 1;
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_column_index_to_letter() {
assert_eq!(FormulaTranslator::column_index_to_letter(0), "A");
assert_eq!(FormulaTranslator::column_index_to_letter(1), "B");
assert_eq!(FormulaTranslator::column_index_to_letter(25), "Z");
assert_eq!(FormulaTranslator::column_index_to_letter(26), "AA");
assert_eq!(FormulaTranslator::column_index_to_letter(27), "AB");
assert_eq!(FormulaTranslator::column_index_to_letter(701), "ZZ");
}
#[test]
fn test_simple_formula_translation() {
let mut column_map = HashMap::new();
column_map.insert("revenue".to_string(), "A".to_string());
column_map.insert("cogs".to_string(), "B".to_string());
let translator = FormulaTranslator::new(column_map);
let result = translator
.translate_row_formula("=revenue - cogs", 2)
.unwrap();
assert_eq!(result, "=A2 - B2");
let result = translator
.translate_row_formula("=revenue / cogs", 3)
.unwrap();
assert_eq!(result, "=A3 / B3");
}
#[test]
fn test_formula_with_multiple_columns() {
let mut column_map = HashMap::new();
column_map.insert("sales_marketing".to_string(), "A".to_string());
column_map.insert("rd".to_string(), "B".to_string());
column_map.insert("ga".to_string(), "C".to_string());
let translator = FormulaTranslator::new(column_map);
let result = translator
.translate_row_formula("=sales_marketing + rd + ga", 2)
.unwrap();
assert_eq!(result, "=A2 + B2 + C2");
}
#[test]
fn test_formula_with_parentheses() {
let mut column_map = HashMap::new();
column_map.insert("gross_profit".to_string(), "A".to_string());
column_map.insert("revenue".to_string(), "B".to_string());
let translator = FormulaTranslator::new(column_map);
let result = translator
.translate_row_formula("=(gross_profit / revenue) * 100", 2)
.unwrap();
assert_eq!(result, "=(A2 / B2) * 100");
}
#[test]
fn test_cross_table_reference() {
let column_map = HashMap::new();
let translator = FormulaTranslator::new(column_map);
let result = translator
.translate_row_formula("=pl_2025.revenue", 2)
.unwrap();
assert_eq!(result, "='pl_2025'!revenue2");
}
#[test]
fn test_formula_without_leading_equals() {
let mut column_map = HashMap::new();
column_map.insert("revenue".to_string(), "A".to_string());
column_map.insert("cogs".to_string(), "B".to_string());
let translator = FormulaTranslator::new(column_map);
let result = translator
.translate_row_formula("revenue - cogs", 2)
.unwrap();
assert_eq!(result, "=A2 - B2");
}
#[test]
fn test_financial_functions_preserved() {
let mut column_map = HashMap::new();
column_map.insert("cashflow".to_string(), "A".to_string());
let translator = FormulaTranslator::new(column_map);
let result = translator
.translate_row_formula("=NPV(0.1, cashflow)", 2)
.unwrap();
assert!(result.contains("NPV"));
assert!(result.contains("A2"));
let result = translator
.translate_row_formula("=XNPV(0.1, cashflow, 45000)", 2)
.unwrap();
assert!(result.contains("XNPV"));
let result = translator
.translate_row_formula("=PMT(0.05, 12, 1000)", 2)
.unwrap();
assert!(result.contains("PMT"));
let result = translator
.translate_row_formula("=IRR(cashflow)", 2)
.unwrap();
assert!(result.contains("IRR"));
let result = translator
.translate_row_formula("=PV(0.1, 10, 100)", 2)
.unwrap();
assert!(result.contains("PV"));
let result = translator
.translate_row_formula("=FV(0.1, 10, 100)", 2)
.unwrap();
assert!(result.contains("FV"));
}
#[test]
fn test_date_functions_preserved() {
let column_map = HashMap::new();
let translator = FormulaTranslator::new(column_map);
let result = translator
.translate_row_formula("=DATEDIF(45000, 45365, 1)", 2)
.unwrap();
assert!(result.contains("DATEDIF"));
let result = translator
.translate_row_formula("=EDATE(45000, 3)", 2)
.unwrap();
assert!(result.contains("EDATE"));
let result = translator
.translate_row_formula("=EOMONTH(45000, 1)", 2)
.unwrap();
assert!(result.contains("EOMONTH"));
}
#[test]
fn test_other_new_functions_preserved() {
let column_map = HashMap::new();
let translator = FormulaTranslator::new(column_map);
let result = translator
.translate_row_formula("=CHOOSE(1, 10, 20, 30)", 2)
.unwrap();
assert!(result.contains("CHOOSE"));
let result = translator.translate_row_formula("=POWER(2, 8)", 2).unwrap();
assert!(result.contains("POWER"));
let result = translator
.translate_row_formula("=CONCAT(1, 2)", 2)
.unwrap();
assert!(result.contains("CONCAT"));
let result = translator
.translate_row_formula("=MAXIFS(1, 2, 3)", 2)
.unwrap();
assert!(result.contains("MAXIFS"));
let result = translator
.translate_row_formula("=MINIFS(1, 2, 3)", 2)
.unwrap();
assert!(result.contains("MINIFS"));
}
#[test]
fn test_new_with_tables() {
let column_map = HashMap::new();
let mut table_column_maps = HashMap::new();
let mut sales_cols = HashMap::new();
sales_cols.insert("revenue".to_string(), "A".to_string());
sales_cols.insert("cost".to_string(), "B".to_string());
table_column_maps.insert("sales".to_string(), sales_cols);
let mut table_row_counts = HashMap::new();
table_row_counts.insert("sales".to_string(), 5);
let translator =
FormulaTranslator::new_with_tables(column_map, table_column_maps, table_row_counts);
let result = translator
.translate_row_formula("=sales.revenue", 2)
.unwrap();
assert!(result.contains("sales"));
}
#[test]
fn test_translate_scalar_formula_basic() {
let mut table_column_maps = HashMap::new();
let mut sales_cols = HashMap::new();
sales_cols.insert("amount".to_string(), "A".to_string());
table_column_maps.insert("sales".to_string(), sales_cols);
let mut table_row_counts = HashMap::new();
table_row_counts.insert("sales".to_string(), 3);
let translator =
FormulaTranslator::new_with_tables(HashMap::new(), table_column_maps, table_row_counts);
let scalar_row_map = HashMap::new();
let result = translator
.translate_scalar_formula("=SUM(sales.amount)", &scalar_row_map)
.unwrap();
assert!(result.contains("SUM"));
assert!(result.contains("sales"));
}
#[test]
fn test_translate_scalar_formula_with_index() {
let mut table_column_maps = HashMap::new();
let mut data_cols = HashMap::new();
data_cols.insert("value".to_string(), "C".to_string());
table_column_maps.insert("data".to_string(), data_cols);
let mut table_row_counts = HashMap::new();
table_row_counts.insert("data".to_string(), 10);
let translator =
FormulaTranslator::new_with_tables(HashMap::new(), table_column_maps, table_row_counts);
let scalar_row_map = HashMap::new();
let result = translator
.translate_scalar_formula("=data.value[0]", &scalar_row_map)
.unwrap();
assert!(result.contains("data"));
assert!(result.contains("C2")); }
#[test]
fn test_translate_scalar_formula_with_scalar_ref() {
let translator = FormulaTranslator::new(HashMap::new());
let mut scalar_row_map = HashMap::new();
scalar_row_map.insert("metrics.total".to_string(), 5);
let result = translator
.translate_scalar_formula("=metrics.total * 2", &scalar_row_map)
.unwrap();
assert!(result.contains("B5")); }
#[test]
fn test_column_not_found_error() {
let column_map = HashMap::new(); let translator = FormulaTranslator::new(column_map);
let result = translator.translate_row_formula("=unknown_column + 1", 2);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("not found"));
}
#[test]
fn test_more_excel_functions_preserved() {
let column_map = HashMap::new();
let translator = FormulaTranslator::new(column_map);
let functions = [
"ABS", "ROUND", "CEILING", "FLOOR", "MOD", "SQRT", "EXP", "LN", "LOG", "LOG10",
];
for func in functions {
let formula = format!("={func}(10)");
let result = translator.translate_row_formula(&formula, 2).unwrap();
assert!(
result.to_uppercase().contains(func),
"Function {func} should be preserved"
);
}
}
#[test]
fn test_text_functions_preserved() {
let column_map = HashMap::new();
let translator = FormulaTranslator::new(column_map);
let functions = ["UPPER", "LOWER", "TRIM", "LEN", "LEFT", "RIGHT", "MID"];
for func in functions {
let formula = format!("={func}(1)"); let result = translator.translate_row_formula(&formula, 2).unwrap();
assert!(
result.to_uppercase().contains(func),
"Function {func} should be preserved"
);
}
}
#[test]
fn test_logical_functions_preserved() {
let column_map = HashMap::new();
let translator = FormulaTranslator::new(column_map);
let functions = ["IF", "AND", "OR", "NOT", "TRUE", "FALSE", "IFERROR"];
for func in functions {
let formula = format!("={func}(1, 2, 3)");
let result = translator.translate_row_formula(&formula, 2).unwrap();
assert!(
result.to_uppercase().contains(func),
"Function {func} should be preserved"
);
}
}
#[test]
fn test_column_index_to_letter_extended() {
assert_eq!(FormulaTranslator::column_index_to_letter(702), "AAA");
assert_eq!(FormulaTranslator::column_index_to_letter(52), "BA");
}
#[test]
fn test_is_excel_function() {
assert!(FormulaTranslator::is_excel_function("SUM"));
assert!(FormulaTranslator::is_excel_function("sum")); assert!(FormulaTranslator::is_excel_function("AVERAGE"));
assert!(FormulaTranslator::is_excel_function("NPV"));
assert!(FormulaTranslator::is_excel_function("XLOOKUP"));
assert!(!FormulaTranslator::is_excel_function("revenue")); assert!(!FormulaTranslator::is_excel_function("my_column")); }
#[test]
fn test_translate_sumifs_formula() {
let mut table_column_maps = HashMap::new();
let mut sales_cols = HashMap::new();
sales_cols.insert("amount".to_string(), "A".to_string());
sales_cols.insert("region".to_string(), "B".to_string());
sales_cols.insert("product".to_string(), "C".to_string());
table_column_maps.insert("sales_data".to_string(), sales_cols);
let mut table_row_counts = HashMap::new();
table_row_counts.insert("sales_data".to_string(), 6);
let translator =
FormulaTranslator::new_with_tables(HashMap::new(), table_column_maps, table_row_counts);
let scalar_row_map = HashMap::new();
let result = translator
.translate_scalar_formula(
"=SUMIFS(sales_data.amount, sales_data.region, 1, sales_data.product, 1)",
&scalar_row_map,
)
.unwrap();
println!("SUMIFS result: {result}");
assert!(result.contains("'sales_data'!A2:A7"));
assert!(result.contains("'sales_data'!B2:B7"));
assert!(result.contains("'sales_data'!C2:C7"));
}
#[test]
fn test_translate_percentile_formula() {
let mut table_column_maps = HashMap::new();
let mut dataset_cols = HashMap::new();
dataset_cols.insert("values".to_string(), "A".to_string());
table_column_maps.insert("dataset".to_string(), dataset_cols);
let mut table_row_counts = HashMap::new();
table_row_counts.insert("dataset".to_string(), 10);
let translator =
FormulaTranslator::new_with_tables(HashMap::new(), table_column_maps, table_row_counts);
let scalar_row_map = HashMap::new();
let result = translator
.translate_scalar_formula("=PERCENTILE(dataset.values, 0.5)", &scalar_row_map)
.unwrap();
println!("PERCENTILE result: {result}");
assert!(result.contains("'dataset'!A2:A11"));
}
#[test]
fn test_translate_correl_formula() {
let mut table_column_maps = HashMap::new();
let mut data_cols = HashMap::new();
data_cols.insert("advertising".to_string(), "A".to_string());
data_cols.insert("sales".to_string(), "B".to_string());
table_column_maps.insert("data_series".to_string(), data_cols);
let mut table_row_counts = HashMap::new();
table_row_counts.insert("data_series".to_string(), 5);
let translator =
FormulaTranslator::new_with_tables(HashMap::new(), table_column_maps, table_row_counts);
let scalar_row_map = HashMap::new();
let result = translator
.translate_scalar_formula(
"=CORREL(data_series.advertising, data_series.sales)",
&scalar_row_map,
)
.unwrap();
println!("CORREL result: {result}");
assert!(result.contains("'data_series'!A2:A6"));
assert!(result.contains("'data_series'!B2:B6"));
}
}