FIX: Fixes several issues with DATABASE functions

Fixes #547
This commit is contained in:
Nicolás Hatcher
2025-11-14 23:41:43 +01:00
committed by Nicolás Hatcher Andrés
parent 129959137d
commit c52c05aa8e
2 changed files with 233 additions and 105 deletions

View File

@@ -1,6 +1,9 @@
use chrono::Datelike;
use crate::{
calc_result::CalcResult,
expressions::{parser::Node, token::Error, types::CellReferenceIndex},
formatter::dates::date_to_serial_number,
Model,
};
@@ -28,6 +31,15 @@ impl Model {
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut sum = 0.0f64;
let mut count = 0usize;
@@ -82,6 +94,15 @@ impl Model {
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut count = 0usize;
let mut row = db_left.row + 1; // skip header
while row <= db_right.row {
@@ -123,10 +144,19 @@ impl Model {
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut result: Option<CalcResult> = None;
let mut matches = 0usize;
let mut row = db_left.row + 1; // skip header
let mut row = db_left.row + 1;
while row <= db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
matches += 1;
@@ -187,7 +217,16 @@ impl Model {
Err(e) => return e,
};
let mut sum = 0.0f64;
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut sum = 0.0;
// skip header
let mut row = db_left.row + 1;
@@ -220,42 +259,80 @@ impl Model {
field_arg: &Node,
cell: CellReferenceIndex,
) -> Result<i32, CalcResult> {
// If numeric -> index
if let Ok(n) = self.get_number(field_arg, cell) {
let idx = if n < 1.0 {
n.ceil() as i32
} else {
n.floor() as i32
};
if idx < 1 || db_left.column + idx - 1 > db_right.column {
return Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Field index out of range".to_string(),
});
let field_column_name = match self.evaluate_node_in_context(field_arg, cell) {
CalcResult::String(s) => s.to_lowercase(),
CalcResult::Number(index) => {
let index = index.floor() as i32;
if index < 1 || db_left.column + index - 1 > db_right.column {
return Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Field index out of range".to_string(),
});
}
return Ok(db_left.column + index - 1);
}
CalcResult::Boolean(b) => {
return if b {
Ok(1)
} else {
// Index 0 is out of range
Err(CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid field specifier".to_string(),
})
};
}
error @ CalcResult::Error { .. } => {
return Err(error);
}
CalcResult::Range { .. } => {
return Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
})
}
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return Err(CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
})
}
return Ok(db_left.column + idx - 1);
}
// Otherwise treat as header name
let wanted = match self.get_string(field_arg, cell) {
Ok(s) => s.to_lowercase(),
Err(e) => return Err(e),
};
let mut col = db_left.column;
while col <= db_right.column {
// We search in the database a column whose header matches field_column_name
for column in db_left.column..=db_right.column {
let v = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row: db_left.row,
column: col,
column,
});
if let CalcResult::String(s) = v {
if s.to_lowercase() == wanted {
return Ok(col);
match &v {
CalcResult::String(s) => {
if s.to_lowercase() == field_column_name {
return Ok(column);
}
}
CalcResult::Number(n) => {
if field_column_name == n.to_string() {
return Ok(column);
}
}
CalcResult::Boolean(b) => {
if field_column_name == b.to_string() {
return Ok(column);
}
}
CalcResult::Error { .. }
| CalcResult::Range { .. }
| CalcResult::EmptyCell
| CalcResult::EmptyArg
| CalcResult::Array(_) => {}
}
col += 1;
}
Err(CalcResult::Error {
@@ -278,53 +355,84 @@ impl Model {
// Read criteria headers (first row of criteria range)
// Map header name (lowercased) -> db column (if exists)
let mut crit_cols: Vec<Option<i32>> = Vec::new();
let mut col = c_left.column;
while col <= c_right.column {
let h = self.evaluate_cell(CellReferenceIndex {
let mut crit_cols: Vec<i32> = Vec::new();
let mut header_count = 0;
// We cover the criteria table:
// headerA | headerB | ...
// critA1 | critA2 | ...
// critB1 | critB2 | ...
// ...
for column in c_left.column..=c_right.column {
let cell = CellReferenceIndex {
sheet: c_left.sheet,
row: c_left.row,
column: col,
});
let db_col = if let CalcResult::String(s) = h {
column,
};
let criteria_header = self.evaluate_cell(cell);
if let Ok(s) = self.cast_to_string(criteria_header, cell) {
// Non-empty string header. If the header is non string we skip it
header_count += 1;
let wanted = s.to_lowercase();
// Find corresponding DB column
let mut db_c = db_left.column;
let mut found: Option<i32> = None;
while db_c <= db_right.column {
let hdr = self.evaluate_cell(CellReferenceIndex {
// Find corresponding Database column
let mut found = false;
for db_column in db_left.column..=db_right.column {
let db_header = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row: db_left.row,
column: db_c,
column: db_column,
});
if let CalcResult::String(hs) = hdr {
if let Ok(hs) = self.cast_to_string(db_header, cell) {
if hs.to_lowercase() == wanted {
found = Some(db_c);
crit_cols.push(db_column);
found = true;
break;
}
}
db_c += 1;
}
found
} else {
None
if !found {
// that means the criteria column has no matching DB column
// If the criteria condition is empty then we remove this condition
// otherwise this condition can never be satisfied
// We evaluate all criteria rows to see if any is non-empty
let mut has_non_empty = false;
for r in (c_left.row + 1)..=c_right.row {
let ccell = self.evaluate_cell(CellReferenceIndex {
sheet: c_left.sheet,
row: r,
column,
});
if !matches!(ccell, CalcResult::EmptyCell | CalcResult::EmptyArg) {
has_non_empty = true;
break;
}
}
if has_non_empty {
// This criteria column can never be satisfied
header_count -= 1;
}
}
};
crit_cols.push(db_col);
col += 1;
}
// If no criteria rows (only headers), everything matches
if c_right.row <= c_left.row {
// If no criteria rows (only headers), everything matches
return true;
}
if header_count == 0 {
// If there are not "String" headers, nothing matches
// NB: There might be String headers that do not match any DB columns,
// in that case everything matches.
return false;
}
// Evaluate each criteria row (OR)
let mut r = c_left.row + 1;
while r <= c_right.row {
for r in (c_left.row + 1)..=c_right.row {
// AND across columns for this criteria row
let mut and_ok = true;
for (offset, maybe_db_col) in crit_cols.iter().enumerate() {
for (offset, db_col) in crit_cols.iter().enumerate() {
// Criteria cell
let ccell = self.evaluate_cell(CellReferenceIndex {
sheet: c_left.sheet,
@@ -337,17 +445,11 @@ impl Model {
continue;
}
// Header without mapping -> ignore this criteria column (Excel ignores unknown headers)
let db_col = match maybe_db_col {
Some(c) => *c,
None => continue,
};
// Database value for this row/column
let db_val = self.evaluate_cell(CellReferenceIndex {
sheet: db_left.sheet,
row,
column: db_col,
column: *db_col,
});
if !self.criteria_cell_matches(&db_val, &ccell) {
@@ -360,8 +462,6 @@ impl Model {
// This criteria row satisfied (OR)
return true;
}
r += 1;
}
// none matched
@@ -373,44 +473,67 @@ impl Model {
fn criteria_cell_matches(&self, db_val: &CalcResult, crit_cell: &CalcResult) -> bool {
// Convert the criteria cell to a string for operator parsing if possible,
// otherwise fall back to equality via compare_values.
let crit_str_opt = match crit_cell {
CalcResult::String(s) => Some(s.clone()),
CalcResult::Number(n) => Some(n.to_string()),
CalcResult::Boolean(b) => Some(if *b {
"TRUE".to_string()
} else {
"FALSE".to_string()
}),
CalcResult::EmptyCell | CalcResult::EmptyArg => return true,
let mut criteria = match crit_cell {
CalcResult::String(s) => s.trim().to_string(),
CalcResult::Number(n) => {
// treat as equality with number
return match db_val {
CalcResult::Number(v) => (*v - *n).abs() <= f64::EPSILON,
_ => false,
};
}
CalcResult::Boolean(b) => {
// check equality with boolean
return match db_val {
CalcResult::Boolean(v) => *v == *b,
_ => false,
};
}
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Error { .. } => return false,
CalcResult::Range { .. } | CalcResult::Array(_) => return false,
};
if crit_str_opt.is_none() {
return compare_values(db_val, crit_cell) == 0;
}
let mut crit = match crit_str_opt {
Some(s) => s.trim().to_string(),
None => return false,
};
// Detect operator prefix
let mut op = "="; // default equality (with wildcard semantics for strings)
let prefixes = ["<>", ">=", "<=", ">", "<", "="];
for p in prefixes.iter() {
if crit.starts_with(p) {
if criteria.starts_with(p) {
op = p;
crit = crit[p.len()..].trim().to_string();
criteria = criteria[p.len()..].trim().to_string();
break;
}
}
// Try to parse numeric RHS
let rhs_num = crit.parse::<f64>().ok();
// Is it a number?
let rhs_num = criteria.parse::<f64>().ok();
// Is it a date?
// FIXME: We should parse dates according to locale settings
let rhs_date = criteria.parse::<chrono::NaiveDate>().ok();
match op {
">" | ">=" | "<" | "<=" => {
if let Some(t) = rhs_num {
if let Some(d) = rhs_date {
// date comparison
let serial = match date_to_serial_number(d.day(), d.month(), d.year()) {
Ok(sn) => sn as f64,
Err(_) => return false,
};
if let CalcResult::Number(n) = db_val {
match op {
">" => *n > serial,
">=" => *n >= serial,
"<" => *n < serial,
"<=" => *n <= serial,
_ => false,
}
} else {
false
}
} else if let Some(t) = rhs_num {
// numeric comparison
if let CalcResult::Number(n) = db_val {
match op {
@@ -421,7 +544,6 @@ impl Model {
_ => false,
}
} else {
// For non-numbers, use compare_values with a number token to emulate Excel ordering
let rhs = CalcResult::Number(t);
let c = compare_values(db_val, &rhs);
match op {
@@ -434,7 +556,7 @@ impl Model {
}
} else {
// string comparison (case-insensitive) using compare_values semantics
let rhs = CalcResult::String(crit.to_lowercase());
let rhs = CalcResult::String(criteria.to_lowercase());
let lhs = match db_val {
CalcResult::String(s) => CalcResult::String(s.to_lowercase()),
x => x.clone(),
@@ -453,8 +575,8 @@ impl Model {
// not equal (with wildcard semantics for strings)
// If rhs has wildcards and db_val is string, do regex; else use compare_values != 0
if let CalcResult::String(s) = db_val {
if crit.contains('*') || crit.contains('?') {
if let Ok(re) = from_wildcard_to_regex(&crit.to_lowercase(), true) {
if criteria.contains('*') || criteria.contains('?') {
if let Ok(re) = from_wildcard_to_regex(&criteria.to_lowercase(), true) {
return !result_matches_regex(
&CalcResult::String(s.to_lowercase()),
&re,
@@ -465,7 +587,7 @@ impl Model {
let rhs = if let Some(n) = rhs_num {
CalcResult::Number(n)
} else {
CalcResult::String(crit.to_lowercase())
CalcResult::String(criteria.to_lowercase())
};
let lhs = match db_val {
CalcResult::String(s) => CalcResult::String(s.to_lowercase()),
@@ -485,18 +607,19 @@ impl Model {
} else {
// textual/boolean equals (case-insensitive), wildcard-enabled for strings
if let CalcResult::String(s) = db_val {
if crit.contains('*') || crit.contains('?') {
if let Ok(re) = from_wildcard_to_regex(&crit.to_lowercase(), true) {
if criteria.contains('*') || criteria.contains('?') {
if let Ok(re) = from_wildcard_to_regex(&criteria.to_lowercase(), true) {
return result_matches_regex(
&CalcResult::String(s.to_lowercase()),
&re,
);
}
}
return s.to_lowercase() == crit.to_lowercase();
// This is weird but we only need to check if "starts with" for equality
return s.to_lowercase().starts_with(&criteria.to_lowercase());
}
// Fallback: compare_values equality
compare_values(db_val, &CalcResult::String(crit.to_lowercase())) == 0
compare_values(db_val, &CalcResult::String(criteria.to_lowercase())) == 0
}
}
}
@@ -528,9 +651,18 @@ impl Model {
Err(e) => return e,
};
if db_right.row <= db_left.row {
// no data rows
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No data rows in database".to_string(),
};
}
let mut best: Option<f64> = None;
let mut row = db_left.row + 1; // skip header
let mut row = db_left.row + 1;
while row <= db_right.row {
if self.db_row_matches_criteria(db_left, db_right, row, criteria) {
let v = self.evaluate_cell(CellReferenceIndex {
@@ -538,15 +670,15 @@ impl Model {
row,
column: field_col,
});
if let CalcResult::Number(n) = v {
if n.is_finite() {
if let CalcResult::Number(value) = v {
if value.is_finite() {
best = Some(match best {
None => n,
None => value,
Some(cur) => {
if want_max {
n.max(cur)
value.max(cur)
} else {
n.min(cur)
value.min(cur)
}
}
});
@@ -558,11 +690,7 @@ impl Model {
match best {
Some(v) => CalcResult::Number(v),
None => CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "No numeric values matched criteria".to_string(),
},
None => CalcResult::Number(0.0),
}
}
}