diff --git a/base/src/functions/database.rs b/base/src/functions/database.rs index 3e47854..d38727a 100644 --- a/base/src/functions/database.rs +++ b/base/src/functions/database.rs @@ -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 = 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 { - // 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> = Vec::new(); - let mut col = c_left.column; - while col <= c_right.column { - let h = self.evaluate_cell(CellReferenceIndex { + let mut crit_cols: Vec = 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 = 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::().ok(); + // Is it a number? + let rhs_num = criteria.parse::().ok(); + + // Is it a date? + // FIXME: We should parse dates according to locale settings + let rhs_date = criteria.parse::().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 = 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), } } } diff --git a/xlsx/tests/calc_tests/DMIN_DMAX_DAVERAGE_DSUM_DCOUNT_DGET.xlsx b/xlsx/tests/calc_tests/DMIN_DMAX_DAVERAGE_DSUM_DCOUNT_DGET.xlsx new file mode 100644 index 0000000..79a07da Binary files /dev/null and b/xlsx/tests/calc_tests/DMIN_DMAX_DAVERAGE_DSUM_DCOUNT_DGET.xlsx differ