From b67c93b1df543e2ce74d4e6bb593e7527e26955e Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Thu, 20 Jun 2024 10:27:45 +0530 Subject: [PATCH 01/14] poc: import formulas when importing xlsx file --- .../execute_operation/execute_code.rs | 3 - .../src/controller/operations/import.rs | 64 ++++++++++++++----- .../src/controller/user_actions/import.rs | 4 +- 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs index 4c606308cf..6c9bda68b6 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs @@ -97,9 +97,6 @@ impl GridController { op: Operation, ) { if let Operation::ComputeCode { sheet_pos } = op { - if !transaction.is_user() { - unreachable!("Only a user transaction should have a ComputeCode"); - } let sheet_id = sheet_pos.sheet_id; let Some(sheet) = self.try_sheet(sheet_id) else { // sheet may have been deleted in a multiplayer operation diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index 468a4e7bbf..f83c9f9f12 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -3,7 +3,10 @@ use std::io::Cursor; use anyhow::{anyhow, bail, Result}; use crate::{ - cell_values::CellValues, controller::GridController, grid::SheetId, CellValue, Pos, SheetPos, + cell_values::CellValues, + controller::GridController, + grid::{CodeCellLanguage, SheetId}, + CellValue, Pos, SheetPos, }; use bytes::Bytes; use calamine::{Data as ExcelData, Reader as ExcelReader, Xlsx, XlsxError}; @@ -126,15 +129,22 @@ impl GridController { file_name: &str, ) -> Result> { let mut ops = vec![] as Vec; - let insert_at = Pos::default(); - let mut cell_values = vec![]; + let mut formula_ops = vec![] as Vec; let error = |message: String| anyhow!("Error parsing Excel file {}: {}", file_name, message); let cursor = Cursor::new(file); let mut workbook: Xlsx<_> = - ExcelReader::new(cursor).map_err(|e: XlsxError| error(e.to_string()))?; + ExcelReader::new(cursor.clone()).map_err(|e: XlsxError| error(e.to_string()))?; let sheets = workbook.sheet_names().to_owned(); + let xlsx_range_to_pos = |(row, col)| Pos { + // col is 0-based in quadratic i.e. A1 is col 0, B1 is col 1, same as excel + x: col as i64, + // row is 1-based in quadratic i.e. A1 is row 1, A2 is row 2 + // it is 0-based in excel i.e. A1 is row 0, A2 is row 1 + // so we add 1 to it, to make cell references work in formulas + y: row as i64 + 1, + }; for sheet_name in sheets { // add the sheet @@ -144,14 +154,17 @@ impl GridController { let sheet_id = sheet.id; ops.extend(add_sheet_operations); + // values + let mut cell_values = vec![]; let range = workbook .worksheet_range(&sheet_name) .map_err(|e: XlsxError| error(e.to_string()))?; let size = range.get_size(); + let start = range.start().map_or_else(Pos::default, xlsx_range_to_pos); for row in range.rows() { - for col in row.iter() { - let cell_value = match col { + for cell in row.iter() { + let cell_value = match cell { ExcelData::Empty => CellValue::Blank, ExcelData::String(value) => CellValue::Text(value.to_string()), ExcelData::DateTimeIso(ref value) => CellValue::Text(value.to_string()), @@ -181,23 +194,44 @@ impl GridController { ExcelData::Error(_) => CellValue::Blank, ExcelData::Bool(value) => CellValue::Logical(*value), }; - cell_values.push(cell_value); } } - let values = CellValues::from_flat_array(size.1 as u32, size.0 as u32, cell_values); - let operations = Operation::SetCellValues { - sheet_pos: (insert_at.x, insert_at.y, sheet_id).into(), + let value_op = Operation::SetCellValues { + sheet_pos: (start.x, start.y, sheet_id).into(), values, }; - ops.push(operations); + ops.push(value_op); - // empty cell values for each sheet - cell_values = vec![]; + // formulas + let formula = workbook + .worksheet_formula(&sheet_name) + .map_err(|e: XlsxError| error(e.to_string()))?; + let start = formula.start().map_or_else(Pos::default, xlsx_range_to_pos); + for (row_idx, row) in formula.rows().enumerate() { + for (col_idx, cell) in row.iter().enumerate() { + if !cell.is_empty() { + let sheet_pos = SheetPos { + x: start.x + col_idx as i64, + y: start.y + row_idx as i64, + sheet_id, + }; + let code_cell_ops = self.set_code_cell_operations( + sheet_pos, + CodeCellLanguage::Formula, + cell.to_string(), + ); + formula_ops.extend(code_cell_ops); + } + } + } } } + // formula ops are added after all set value ops + // so that all cell values are populated in all sheets before formulas are evaluated + ops.extend(formula_ops); Ok(ops) } @@ -295,15 +329,15 @@ fn read_utf16(bytes: &[u8]) -> Option { // strip invalid characters let result: String = str.chars().filter(|&c| c.len_utf8() <= 2).collect(); - + Some(result) } #[cfg(test)] mod test { use super::read_utf16; - use crate::CellValue; use super::*; + use crate::CellValue; const INVALID_ENCODING_FILE: &[u8] = include_bytes!("../../../../quadratic-rust-shared/data/csv/encoding_issue.csv"); diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index f2588c9158..8318cffa8d 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -187,7 +187,7 @@ mod tests { sheet_id, 0, 10, - 0, + 1, vec![ "Empty", "String", @@ -208,7 +208,7 @@ mod tests { sheet_id, 0, 10, - 1, + 2, vec![ "", "Hello", From bf6c34426b76d0a518dd9615102987f395ed39ab Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Fri, 21 Jun 2024 22:04:38 +0530 Subject: [PATCH 02/14] cleanup --- quadratic-core/src/controller/operations/import.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index ced865bfcc..0f679db293 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -136,10 +136,11 @@ impl GridController { let cursor = Cursor::new(file); let mut workbook: Xlsx<_> = - ExcelReader::new(cursor.clone()).map_err(|e: XlsxError| error(e.to_string()))?; + ExcelReader::new(cursor).map_err(|e: XlsxError| error(e.to_string()))?; let sheets = workbook.sheet_names().to_owned(); + // first cell in excel is A1, but first cell in quadratic is A0 - // so we need to offset rows by 1, so that values are inserted in the correct position + // so we need to offset rows by 1, so that values are inserted in the original A1 notations cell // this is required so that cell references (A1 notations) in formulas are correct let xlsx_range_to_pos = |(row, col)| Pos { x: col as i64, From 87882d2b7846ba06acb7ba91c67977955a51fc17 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Fri, 21 Jun 2024 22:20:03 +0530 Subject: [PATCH 03/14] rerun all code cell in order --- quadratic-core/src/controller/operations/import.rs | 7 +++---- quadratic-core/src/controller/user_actions/import.rs | 9 +++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index 0f679db293..6e70f30e68 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -130,7 +130,6 @@ impl GridController { file_name: &str, ) -> Result> { let mut ops = vec![] as Vec; - let mut formula_compute_ops = vec![] as Vec; let error = |message: String| anyhow!("Error parsing Excel file {}: {}", file_name, message); @@ -206,6 +205,7 @@ impl GridController { .worksheet_formula(&sheet_name) .map_err(|e: XlsxError| error(e.to_string()))?; let insert_at = formula.start().map_or_else(Pos::default, xlsx_range_to_pos); + let mut formula_compute_ops = vec![] as Vec; for (y, row) in formula.rows().enumerate() { for (x, cell) in row.iter().enumerate() { if !cell.is_empty() { @@ -218,7 +218,7 @@ impl GridController { code: cell.to_string(), }); sheet.set_cell_value(pos, cell_value); - // add the compute operation, to generate code runs + // add code compute operation, to generate code runs formula_compute_ops.push(Operation::ComputeCode { sheet_pos: pos.to_sheet_pos(sheet.id), }); @@ -228,9 +228,8 @@ impl GridController { ops.push(Operation::AddSheetSchema { schema: export_sheet(&sheet), }); + ops.extend(formula_compute_ops); } - // add formula ops at the end, to ensure all cells values in all sheets are populated - ops.extend(formula_compute_ops); Ok(ops) } diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index bc54f6b058..8f3fb2b8e6 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -22,8 +22,13 @@ impl GridController { /// /// Returns a [`TransactionSummary`]. pub fn import_excel(&mut self, file: Vec, file_name: &str) -> Result<()> { - let ops = self.import_excel_operations(file, file_name)?; - self.server_apply_transaction(ops); + let import_ops = self.import_excel_operations(file, file_name)?; + self.server_apply_transaction(import_ops); + + // Rerun all code cells after importing Excel file + // This is required to run compute cells in order + let code_rerun_ops = self.rerun_all_code_cells_operations(); + self.server_apply_transaction(code_rerun_ops); Ok(()) } From 1ebdf6afce2eda29bdd0f7b5261e3f83f0da8587 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Fri, 21 Jun 2024 22:57:51 +0530 Subject: [PATCH 04/14] add unreachable code compute check for undo redo and multiplayer transaction --- .../src/controller/execution/execute_operation/execute_code.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs index 6c9bda68b6..9f6cd36c5f 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs @@ -97,6 +97,9 @@ impl GridController { op: Operation, ) { if let Operation::ComputeCode { sheet_pos } = op { + if !transaction.is_user() && !transaction.is_server() { + unreachable!("Only a user/server transaction should have a ComputeCode"); + } let sheet_id = sheet_pos.sheet_id; let Some(sheet) = self.try_sheet(sheet_id) else { // sheet may have been deleted in a multiplayer operation From ad8ee32415eb5313095bfb755d0a72eb02e64bec Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Tue, 25 Jun 2024 14:59:00 +0530 Subject: [PATCH 05/14] propagate run errors --- .../execution/run_code/get_cells.rs | 10 ++-- .../src/controller/execution/run_code/mod.rs | 17 ++---- .../execution/run_code/run_formula.rs | 57 +++++++------------ 3 files changed, 33 insertions(+), 51 deletions(-) diff --git a/quadratic-core/src/controller/execution/run_code/get_cells.rs b/quadratic-core/src/controller/execution/run_code/get_cells.rs index 53aaf07707..e80b0c5a2e 100644 --- a/quadratic-core/src/controller/execution/run_code/get_cells.rs +++ b/quadratic-core/src/controller/execution/run_code/get_cells.rs @@ -3,7 +3,7 @@ use uuid::Uuid; use crate::{ controller::{execution::TransactionType, GridController}, error_core::CoreError, - Rect, + Rect, RunError, RunErrorMsg, }; use serde::{Deserialize, Serialize}; @@ -48,12 +48,14 @@ impl GridController { } else { // unable to find sheet by name, generate error let mut msg = format!("Sheet '{}' not found", sheet_name); - if let Some(line_number) = line_number { msg = format!("{} at line {}", msg, line_number); } - - let error = match self.code_cell_sheet_error(&mut transaction, &msg, line_number) { + let run_error = RunError { + span: None, + msg: RunErrorMsg::PythonError(msg.clone().into()), + }; + let error = match self.code_cell_sheet_error(&mut transaction, &run_error) { Ok(_) => CoreError::CodeCellSheetError(msg.to_owned()), Err(err) => err, }; diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index 85bde8cebd..7282ebfa66 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -213,8 +213,7 @@ impl GridController { pub(super) fn code_cell_sheet_error( &mut self, transaction: &mut PendingTransaction, - error_msg: &str, - line_number: Option, + error: &RunError, ) -> Result<()> { let sheet_pos = match transaction.current_sheet_pos { Some(sheet_pos) => sheet_pos, @@ -242,13 +241,7 @@ impl GridController { return Ok(()); } - let msg = RunErrorMsg::PythonError(error_msg.to_owned().into()); - let span = line_number.map(|line_number| Span { - start: line_number, - end: line_number, - }); - let error = RunError { span, msg }; - let result = CodeRunResult::Err(error); + let result = CodeRunResult::Err(error.clone()); let new_code_run = match sheet.code_run(pos) { Some(old_code_run) => { @@ -259,7 +252,7 @@ impl GridController { line_number: old_code_run.line_number, output_type: old_code_run.output_type.clone(), std_out: None, - std_err: Some(error_msg.to_owned()), + std_err: Some(error.msg.to_string()), spill_error: false, last_modified: Utc::now(), @@ -271,10 +264,10 @@ impl GridController { formatted_code_string: None, result, return_type: None, - line_number, + line_number: error.span.map(|span| span.start), output_type: None, std_out: None, - std_err: Some(error_msg.to_owned()), + std_err: Some(error.msg.to_string()), spill_error: false, last_modified: Utc::now(), cells_accessed: transaction.cells_accessed.clone(), diff --git a/quadratic-core/src/controller/execution/run_code/run_formula.rs b/quadratic-core/src/controller/execution/run_code/run_formula.rs index cf7c1ffec3..8306109e30 100644 --- a/quadratic-core/src/controller/execution/run_code/run_formula.rs +++ b/quadratic-core/src/controller/execution/run_code/run_formula.rs @@ -17,42 +17,29 @@ impl GridController { let mut ctx = Ctx::new(self.grid(), sheet_pos); transaction.current_sheet_pos = Some(sheet_pos); match parse_formula(&code, sheet_pos.into()) { - Ok(parsed) => { - match parsed.eval(&mut ctx, false) { - Ok(value) => { - transaction.cells_accessed = ctx.cells_accessed; - let new_code_run = CodeRun { - std_out: None, - std_err: None, - formatted_code_string: None, - spill_error: false, - last_modified: Utc::now(), - cells_accessed: transaction.cells_accessed.clone(), - result: CodeRunResult::Ok(value), - return_type: None, - line_number: None, - output_type: None, - }; - self.finalize_code_run(transaction, sheet_pos, Some(new_code_run), None); - } - Err(error) => { - let msg = error.msg.to_string(); - let line_number = error.span.map(|span| span.start); - - // todo: propagate the result - let _ = self.code_cell_sheet_error( - transaction, - &msg, - // todo: span should be multiline - line_number, - ); - } + Ok(parsed) => match parsed.eval(&mut ctx, false) { + Ok(value) => { + transaction.cells_accessed = ctx.cells_accessed; + let new_code_run = CodeRun { + std_out: None, + std_err: None, + formatted_code_string: None, + spill_error: false, + last_modified: Utc::now(), + cells_accessed: transaction.cells_accessed.clone(), + result: CodeRunResult::Ok(value), + return_type: None, + line_number: None, + output_type: None, + }; + self.finalize_code_run(transaction, sheet_pos, Some(new_code_run), None); } - } - Err(e) => { - let msg = e.to_string(); - // todo: propagate the result - let _ = self.code_cell_sheet_error(transaction, &msg, None); + Err(error) => { + let _ = self.code_cell_sheet_error(transaction, &error); + } + }, + Err(error) => { + let _ = self.code_cell_sheet_error(transaction, &error); } } } From 39153fe9c32061d66699afefb85979078391ddbb Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Tue, 25 Jun 2024 16:53:03 +0530 Subject: [PATCH 06/14] add excel functions list --- .../src/formulas/functions/excel.rs | 529 ++++++++++++++++++ quadratic-core/src/formulas/functions/mod.rs | 1 + 2 files changed, 530 insertions(+) create mode 100644 quadratic-core/src/formulas/functions/excel.rs diff --git a/quadratic-core/src/formulas/functions/excel.rs b/quadratic-core/src/formulas/functions/excel.rs new file mode 100644 index 0000000000..92e98cf033 --- /dev/null +++ b/quadratic-core/src/formulas/functions/excel.rs @@ -0,0 +1,529 @@ +use std::collections::HashSet; + +use lazy_static::lazy_static; + +pub fn is_valid_excel_function(name: &str) -> bool { + ALL_EXCEL_FUNCTIONS.contains(name.to_ascii_uppercase().as_str()) +} + +lazy_static! { + /// Set containing all excel functions. + static ref ALL_EXCEL_FUNCTIONS: HashSet<&'static str> = { + EXCEL_FUNCTIONS_LIST.iter().cloned().collect::>() + }; +} + +const EXCEL_FUNCTIONS_LIST: [&str; 512] = [ + "ABS", + "ACCRINT", + "ACCRINTM", + "ACOS", + "ACOSH", + "ACOT", + "ACOTH", + "AGGREGATE", + "ADDRESS", + "AMORDEGRC", + "AMORLINC", + "AND", + "ARABIC", + "AREAS", + "ARRAYTOTEXT", + "ASC", + "ASIN", + "ASINH", + "ATAN", + "ATAN2", + "ATANH", + "AVEDEV", + "AVERAGE", + "AVERAGEA", + "AVERAGEIF", + "AVERAGEIFS", + "BAHTTEXT", + "BASE", + "BESSELI", + "BESSELJ", + "BESSELK", + "BESSELY", + "BETADIST", + "BETA.DIST", + "BETAINV", + "BETA.INV", + "BIN2DEC", + "BIN2HEX", + "BIN2OCT", + "BINOMDIST", + "BINOM.DIST", + "BINOM.DIST.RANGE", + "BINOM.INV", + "BITAND", + "BITLSHIFT", + "BITOR", + "BITRSHIFT", + "BITXOR", + "BYCOL", + "BYROW", + "CALL", + "CEILING", + "CEILING.MATH", + "CEILING.PRECISE", + "CELL", + "CHAR", + "CHIDIST", + "CHIINV", + "CHITEST", + "CHISQ.DIST", + "CHISQ.DIST.RT", + "CHISQ.INV", + "CHISQ.INV.RT", + "CHISQ.TEST", + "CHOOSE", + "CHOOSECOLS", + "CHOOSEROWS", + "CLEAN", + "CODE", + "COLUMN", + "COLUMNS", + "COMBIN", + "COMBINA", + "COMPLEX", + "CONCAT", + "CONCATENATE", + "CONFIDENCE", + "CONFIDENCE.NORM", + "CONFIDENCE.T", + "CONVERT", + "CORREL", + "COS", + "COSH", + "COT", + "COTH", + "COUNT", + "COUNTA", + "COUNTBLANK", + "COUNTIF", + "COUNTIFS", + "COUPDAYBS", + "COUPDAYS", + "COUPDAYSNC", + "COUPNCD", + "COUPNUM", + "COUPPCD", + "COVAR", + "COVARIANCE.P", + "COVARIANCE.S", + "CRITBINOM", + "CSC", + "CSCH", + "CUBEKPIMEMBER", + "CUBEMEMBER", + "CUBEMEMBERPROPERTY", + "CUBERANKEDMEMBER", + "CUBESET", + "CUBESETCOUNT", + "CUBEVALUE", + "CUMIPMT", + "CUMPRINC", + "DATE", + "DATEDIF", + "DATEVALUE", + "DAVERAGE", + "DAY", + "DAYS", + "DAYS360", + "DB", + "DBCS", + "DCOUNT", + "DCOUNTA", + "DDB", + "DEC2BIN", + "DEC2HEX", + "DEC2OCT", + "DECIMAL", + "DEGREES", + "DELTA", + "DEVSQ", + "DGET", + "DISC", + "DMAX", + "DMIN", + "DOLLAR", + "DOLLARDE", + "DOLLARFR", + "DPRODUCT", + "DROP", + "DSTDEV", + "DSTDEVP", + "DSUM", + "DURATION", + "DVAR", + "DVARP", + "EDATE", + "EFFECT", + "ENCODEURL", + "EOMONTH", + "ERF", + "ERF.PRECISE", + "ERFC", + "ERFC.PRECISE", + "ERROR.TYPE", + "EUROCONVERT", + "EVEN", + "EXACT", + "EXP", + "EXPAND", + "EXPON.DIST", + "EXPONDIST", + "FACT", + "FACTDOUBLE", + "FALSE", + "F.DIST", + "FDIST", + "F.DIST.RT", + "FILTER", + "FILTERXML", + "FIND", + "FINDBS", + "F.INV", + "F.INV.RT", + "FINV", + "FISHER", + "FISHERINV", + "FIXED", + "FLOOR", + "FLOOR.MATH", + "FLOOR.PRECISE", + "FORECAST", + "FORECAST.ETS", + "FORECAST.ETS.CONFINT", + "FORECAST.ETS.SEASONALITY", + "FORECAST.ETS.STAT", + "FORECAST.LINEAR", + "FORMULATEXT", + "FREQUENCY", + "F.TEST", + "FTEST", + "FV", + "FVSCHEDULE", + "GAMMA", + "GAMMA.DIST", + "GAMMADIST", + "GAMMA.INV", + "GAMMAINV", + "GAMMALN", + "GAMMALN.PRECISE", + "GAUSS", + "GCD", + "GEOMEAN", + "GESTEP", + "GETPIVOTDATA", + "GROWTH", + "HARMEAN", + "HEX2BIN", + "HEX2DEC", + "HEX2OCT", + "HLOOKUP", + "HOUR", + "HSTACK", + "HYPERLINK", + "HYPGEOM.DIST", + "HYPGEOMDIST", + "IF", + "IFERROR", + "IFNA", + "IFS", + "IMABS", + "IMAGE", + "IMAGINARY", + "IMARGUMENT", + "IMCONJUGATE", + "IMCOS", + "IMCOSH", + "IMCOT", + "IMCSC", + "IMCSCH", + "IMDIV", + "IMEXP", + "IMLN", + "IMLOG10", + "IMLOG2", + "IMPOWER", + "IMPRODUCT", + "IMREAL", + "IMSEC", + "IMSECH", + "IMSIN", + "IMSINH", + "IMSQRT", + "IMSUB", + "IMSUM", + "IMTAN", + "INDEX", + "INDIRECT", + "INFO", + "INT", + "INTERCEPT", + "INTRATE", + "IPMT", + "IRR", + "ISBLANK", + "ISERR", + "ISERROR", + "ISEVEN", + "ISFORMULA", + "ISLOGICAL", + "ISNA", + "ISNONTEXT", + "ISNUMBER", + "ISODD", + "ISOMITTED", + "ISREF", + "ISTEXT", + "ISO.CEILING", + "ISOWEEKNUM", + "ISPMT", + "JIS", + "KURT", + "LAMBDA", + "LARGE", + "LCM", + "LEFT", + "LEFTBS", + "LEN", + "LENBS", + "LET", + "LINEST", + "LN", + "LOG", + "LOG10", + "LOGEST", + "LOGINV", + "LOGNORM.DIST", + "LOGNORMDIST", + "LOGNORM.INV", + "LOOKUP", + "LOWER", + "MAKEARRAY", + "MAP", + "MATCH", + "MAX", + "MAXA", + "MAXIFS", + "MDETERM", + "MDURATION", + "MEDIAN", + "MID", + "MIDBS", + "MIN", + "MINIFS", + "MINA", + "MINUTE", + "MINVERSE", + "MIRR", + "MMULT", + "MOD", + "MODE", + "MODE.MULT", + "MODE.SNGL", + "MONTH", + "MROUND", + "MULTINOMIAL", + "MUNIT", + "N", + "NA", + "NEGBINOM.DIST", + "NEGBINOMDIST", + "NETWORKDAYS", + "NETWORKDAYS.INTL", + "NOMINAL", + "NORM.DIST", + "NORMDIST", + "NORMINV", + "NORM.INV", + "NORM.S.DIST", + "NORMSDIST", + "NORM.S.INV", + "NORMSINV", + "NOT", + "NOW", + "NPER", + "NPV", + "NUMBERVALUE", + "OCT2BIN", + "OCT2DEC", + "OCT2HEX", + "ODD", + "ODDFPRICE", + "ODDFYIELD", + "ODDLPRICE", + "ODDLYIELD", + "OFFSET", + "OR", + "PDURATION", + "PEARSON", + "PERCENTILE.EXC", + "PERCENTILE.INC", + "PERCENTILE", + "PERCENTRANK.EXC", + "PERCENTRANK.INC", + "PERCENTRANK", + "PERMUT", + "PERMUTATIONA", + "PHI", + "PHONETIC", + "PI", + "PMT", + "POISSON.DIST", + "POISSON", + "POWER", + "PPMT", + "PRICE", + "PRICEDISC", + "PRICEMAT", + "PROB", + "PRODUCT", + "PROPER", + "PV", + "QUARTILE", + "QUARTILE.EXC", + "QUARTILE.INC", + "QUOTIENT", + "RADIANS", + "RAND", + "RANDARRAY", + "RANDBETWEEN", + "RANK.AVG", + "RANK.EQ", + "RANK", + "RATE", + "RECEIVED", + "REDUCE", + "REGISTER.ID", + "REPLACE", + "REPLACEBS", + "REPT", + "RIGHT", + "RIGHTBS", + "ROMAN", + "ROUND", + "ROUNDDOWN", + "ROUNDUP", + "ROW", + "ROWS", + "RRI", + "RSQ", + "RTD", + "SCAN", + "SEARCH", + "SEARCHBS", + "SEC", + "SECH", + "SECOND", + "SEQUENCE", + "SERIESSUM", + "SHEET", + "SHEETS", + "SIGN", + "SIN", + "SINH", + "SKEW", + "SKEW.P", + "SLN", + "SLOPE", + "SMALL", + "SORT", + "SORTBY", + "SQRT", + "SQRTPI", + "STANDARDIZE", + "STOCKHISTORY", + "STDEV", + "STDEV.P", + "STDEV.S", + "STDEVA", + "STDEVP", + "STDEVPA", + "STEYX", + "SUBSTITUTE", + "SUBTOTAL", + "SUM", + "SUMIF", + "SUMIFS", + "SUMPRODUCT", + "SUMSQ", + "SUMX2MY2", + "SUMX2PY2", + "SUMXMY2", + "SWITCH", + "SYD", + "T", + "TAN", + "TANH", + "TAKE", + "TBILLEQ", + "TBILLPRICE", + "TBILLYIELD", + "T.DIST", + "T.DIST.2T", + "T.DIST.RT", + "TDIST", + "TEXT", + "TEXTAFTER", + "TEXTBEFORE", + "TEXTJOIN", + "TEXTSPLIT", + "TIME", + "TIMEVALUE", + "T.INV", + "T.INV.2T", + "TINV", + "TOCOL", + "TOROW", + "TODAY", + "TRANSPOSE", + "TREND", + "TRIM", + "TRIMMEAN", + "TRUE", + "TRUNC", + "T.TEST", + "TTEST", + "TYPE", + "UNICHAR", + "UNICODE", + "UNIQUE", + "UPPER", + "VALUE", + "VALUETOTEXT", + "VAR", + "VAR.P", + "VAR.S", + "VARA", + "VARP", + "VARPA", + "VDB", + "VLOOKUP", + "VSTACK", + "WEBSERVICE", + "WEEKDAY", + "WEEKNUM", + "WEIBULL", + "WEIBULL.DIST", + "WORKDAY", + "WORKDAY.INTL", + "WRAPCOLS", + "WRAPROWS", + "XIRR", + "XLOOKUP", + "XMATCH", + "XNPV", + "XOR", + "YEAR", + "YEARFRAC", + "YIELD", + "YIELDDISC", + "YIELDMAT", + "Z.TEST", + "ZTEST", +]; diff --git a/quadratic-core/src/formulas/functions/mod.rs b/quadratic-core/src/formulas/functions/mod.rs index 74e72f1dc1..6502f82758 100644 --- a/quadratic-core/src/formulas/functions/mod.rs +++ b/quadratic-core/src/formulas/functions/mod.rs @@ -6,6 +6,7 @@ use lazy_static::lazy_static; #[macro_use] mod macros; +pub mod excel; mod logic; mod lookup; mod mathematics; From a24ba43f3018e7fb8343ec41fe07f5be645e7ac2 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Tue, 25 Jun 2024 16:53:54 +0530 Subject: [PATCH 07/14] check for unimplemented excel functions --- quadratic-client/src/app/quadratic-core-types/index.d.ts | 2 +- quadratic-core/src/error_run.rs | 6 +++--- quadratic-core/src/formulas/ast.rs | 9 ++++++++- quadratic-core/src/grid/file/v1_5/run_error.rs | 6 +++--- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/quadratic-client/src/app/quadratic-core-types/index.d.ts b/quadratic-client/src/app/quadratic-core-types/index.d.ts index 21e6708cf1..cfe33ca8f9 100644 --- a/quadratic-client/src/app/quadratic-core-types/index.d.ts +++ b/quadratic-client/src/app/quadratic-core-types/index.d.ts @@ -26,7 +26,7 @@ export type Axis = "X" | "Y"; export interface Instant { seconds: number, } export interface Duration { years: number, months: number, seconds: number, } export interface RunError { span: Span | null, msg: RunErrorMsg, } -export type RunErrorMsg = { "PythonError": string } | "Spill" | "Unimplemented" | "UnknownError" | { "InternalError": string } | { "Unterminated": string } | { "Expected": { expected: string, got: string | null, } } | { "Unexpected": string } | { "TooManyArguments": { func_name: string, max_arg_count: number, } } | { "MissingRequiredArgument": { func_name: string, arg_name: string, } } | "BadFunctionName" | "BadCellReference" | "BadNumber" | { "ExactArraySizeMismatch": { expected: ArraySize, got: ArraySize, } } | { "ExactArrayAxisMismatch": { axis: Axis, expected: number, got: number, } } | { "ArrayAxisMismatch": { axis: Axis, expected: number, got: number, } } | "EmptyArray" | "NonRectangularArray" | "NonLinearArray" | "ArrayTooBig" | "CircularReference" | "Overflow" | "DivideByZero" | "NegativeExponent" | "NotANumber" | "Infinity" | "IndexOutOfBounds" | "NoMatch" | "InvalidArgument"; +export type RunErrorMsg = { "PythonError": string } | "Spill" | { "Unimplemented": string } | "UnknownError" | { "InternalError": string } | { "Unterminated": string } | { "Expected": { expected: string, got: string | null, } } | { "Unexpected": string } | { "TooManyArguments": { func_name: string, max_arg_count: number, } } | { "MissingRequiredArgument": { func_name: string, arg_name: string, } } | "BadFunctionName" | "BadCellReference" | "BadNumber" | { "ExactArraySizeMismatch": { expected: ArraySize, got: ArraySize, } } | { "ExactArrayAxisMismatch": { axis: Axis, expected: number, got: number, } } | { "ArrayAxisMismatch": { axis: Axis, expected: number, got: number, } } | "EmptyArray" | "NonRectangularArray" | "NonLinearArray" | "ArrayTooBig" | "CircularReference" | "Overflow" | "DivideByZero" | "NegativeExponent" | "NotANumber" | "Infinity" | "IndexOutOfBounds" | "NoMatch" | "InvalidArgument"; export interface Pos { x: bigint, y: bigint, } export interface Rect { min: Pos, max: Pos, } export interface Span { start: number, end: number, } diff --git a/quadratic-core/src/error_run.rs b/quadratic-core/src/error_run.rs index 7657a94434..9e4afd178c 100644 --- a/quadratic-core/src/error_run.rs +++ b/quadratic-core/src/error_run.rs @@ -47,7 +47,7 @@ pub enum RunErrorMsg { Spill, // Miscellaneous errors - Unimplemented, + Unimplemented(Cow<'static, str>), UnknownError, InternalError(Cow<'static, str>), @@ -111,8 +111,8 @@ impl fmt::Display for RunErrorMsg { Self::Spill => { write!(f, "Spill error") } - Self::Unimplemented => { - write!(f, "This feature is unimplemented") + Self::Unimplemented(s) => { + write!(f, "This feature is unimplemented: {s}") } Self::UnknownError => { write!(f, "(unknown error)") diff --git a/quadratic-core/src/formulas/ast.rs b/quadratic-core/src/formulas/ast.rs index a4356b75b1..30d22deada 100644 --- a/quadratic-core/src/formulas/ast.rs +++ b/quadratic-core/src/formulas/ast.rs @@ -170,7 +170,14 @@ impl AstNode { let args = FormulaFnArgs::new(arg_values, self.span, f.name); (f.eval)(&mut *ctx, only_parse, args)? } - None => return Err(RunErrorMsg::BadFunctionName.with_span(func.span)), + None => { + if functions::excel::is_valid_excel_function(func_name) { + return Err(RunErrorMsg::Unimplemented(func_name.clone().into()) + .with_span(func.span)); + } else { + return Err(RunErrorMsg::BadFunctionName.with_span(func.span)); + } + } } } diff --git a/quadratic-core/src/grid/file/v1_5/run_error.rs b/quadratic-core/src/grid/file/v1_5/run_error.rs index 4cf21f5bb6..2462b23d3c 100644 --- a/quadratic-core/src/grid/file/v1_5/run_error.rs +++ b/quadratic-core/src/grid/file/v1_5/run_error.rs @@ -23,7 +23,7 @@ pub enum RunErrorMsg { Spill, // Miscellaneous errors - Unimplemented, + Unimplemented(Cow<'static, str>), UnknownError, InternalError(Cow<'static, str>), @@ -90,7 +90,7 @@ impl RunError { msg: match error.msg.clone() { crate::RunErrorMsg::PythonError(str) => RunErrorMsg::PythonError(str), crate::RunErrorMsg::Spill => RunErrorMsg::Spill, - crate::RunErrorMsg::Unimplemented => RunErrorMsg::Unimplemented, + crate::RunErrorMsg::Unimplemented(str) => RunErrorMsg::Unimplemented(str), crate::RunErrorMsg::UnknownError => RunErrorMsg::UnknownError, crate::RunErrorMsg::InternalError(str) => RunErrorMsg::InternalError(str), @@ -184,7 +184,7 @@ impl From for crate::RunError { msg: match error.msg { RunErrorMsg::PythonError(str) => crate::RunErrorMsg::PythonError(str), RunErrorMsg::Spill => crate::RunErrorMsg::Spill, - RunErrorMsg::Unimplemented => crate::RunErrorMsg::Unimplemented, + RunErrorMsg::Unimplemented(str) => crate::RunErrorMsg::Unimplemented(str), RunErrorMsg::UnknownError => crate::RunErrorMsg::UnknownError, RunErrorMsg::InternalError(str) => crate::RunErrorMsg::InternalError(str), From 25e698391346f0325c2bab379ed1a67bf6562ebc Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Tue, 25 Jun 2024 17:00:13 +0530 Subject: [PATCH 08/14] add mixpanel tracking for unimplemented error --- .../src/app/gridGL/cells/CellsArray.ts | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/CellsArray.ts b/quadratic-client/src/app/gridGL/cells/CellsArray.ts index e7741232e5..3f90768fc4 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsArray.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsArray.ts @@ -3,7 +3,8 @@ import { sheets } from '@/app/grid/controller/Sheets'; import { Sheet } from '@/app/grid/sheet/Sheet'; import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler'; import { Coordinate } from '@/app/gridGL/types/size'; -import { JsRenderCodeCell } from '@/app/quadratic-core-types'; +import { JsCodeCell, JsRenderCodeCell, RunError } from '@/app/quadratic-core-types'; +import mixpanel from 'mixpanel-browser'; import { Container, Graphics, ParticleContainer, Point, Rectangle, Sprite, Texture } from 'pixi.js'; import { colors } from '../../theme/colors'; import { dashedTextures } from '../dashedTextures'; @@ -64,14 +65,37 @@ export class CellsArray extends Container { } }; - private updateCodeCell = (options: { sheetId: string; x: number; y: number; renderCodeCell?: JsRenderCodeCell }) => { - if (options.sheetId === this.cellsSheet.sheetId) { - if (options.renderCodeCell) { - this.codeCells.set(this.key(options.x, options.y), options.renderCodeCell); + private updateCodeCell = (options: { + sheetId: string; + x: number; + y: number; + renderCodeCell?: JsRenderCodeCell; + codeCell?: JsCodeCell; + }) => { + const { sheetId, x, y, renderCodeCell, codeCell } = options; + if (sheetId === this.cellsSheet.sheetId) { + if (renderCodeCell) { + this.codeCells.set(this.key(x, y), renderCodeCell); } else { - this.codeCells.delete(this.key(options.x, options.y)); + this.codeCells.delete(this.key(x, y)); } this.create(); + + if (!!codeCell && codeCell.std_err !== null && codeCell.evaluation_result) { + try { + // std_err is not null, so evaluation_result will be RunError + const runError = JSON.parse(codeCell.evaluation_result) as RunError; + // track unimplemented errors + if (typeof runError.msg === 'object' && 'Unimplemented' in runError.msg) { + mixpanel.track('[CellsArray].updateCodeCell', { + type: codeCell.language, + error: runError.msg, + }); + } + } catch (error) { + console.warn('[CellsArray] Error parsing codeCell.evaluation_result', error); + } + } } }; From 98c60106828a3bfd25a3477cd10ff5e4cdec9e38 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Fri, 28 Jun 2024 20:26:51 +0530 Subject: [PATCH 09/14] as per feedback --- quadratic-client/src/app/gridGL/cells/CellsArray.ts | 2 +- .../src/controller/execution/execute_operation/execute_code.rs | 3 ++- quadratic-core/src/controller/operations/import.rs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/quadratic-client/src/app/gridGL/cells/CellsArray.ts b/quadratic-client/src/app/gridGL/cells/CellsArray.ts index 3f90768fc4..d32680549a 100644 --- a/quadratic-client/src/app/gridGL/cells/CellsArray.ts +++ b/quadratic-client/src/app/gridGL/cells/CellsArray.ts @@ -93,7 +93,7 @@ export class CellsArray extends Container { }); } } catch (error) { - console.warn('[CellsArray] Error parsing codeCell.evaluation_result', error); + console.error('[CellsArray] Error parsing codeCell.evaluation_result', error); } } } diff --git a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs index 9f6cd36c5f..9f3afc2b06 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs @@ -98,7 +98,8 @@ impl GridController { ) { if let Operation::ComputeCode { sheet_pos } = op { if !transaction.is_user() && !transaction.is_server() { - unreachable!("Only a user/server transaction should have a ComputeCode"); + dbgjs!("Only a user/server transaction should have a ComputeCode"); + return; } let sheet_id = sheet_pos.sheet_id; let Some(sheet) = self.try_sheet(sheet_id) else { diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index 05f0b566f0..fa0b38e1a8 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -205,7 +205,7 @@ impl GridController { .worksheet_formula(&sheet_name) .map_err(|e: XlsxError| error(e.to_string()))?; let insert_at = formula.start().map_or_else(Pos::default, xlsx_range_to_pos); - let mut formula_compute_ops = vec![] as Vec; + let mut formula_compute_ops = vec![]; for (y, row) in formula.rows().enumerate() { for (x, cell) in row.iter().enumerate() { if !cell.is_empty() { From f8ee1313cdfd2b96548a3f43bda7faf265d40262 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Sat, 29 Jun 2024 04:41:12 +0530 Subject: [PATCH 10/14] add test for excel functions --- .../src/controller/operations/import.rs | 19 +++--- .../src/controller/user_actions/import.rs | 55 +++++++++++++++++- .../src/formulas/functions/excel.rs | 13 ++++- quadratic-core/src/formulas/functions/mod.rs | 6 +- quadratic-core/src/formulas/lexer.rs | 3 +- .../data/excel/all_excel_functions.xlsx | Bin 0 -> 22310 bytes 6 files changed, 83 insertions(+), 13 deletions(-) create mode 100644 quadratic-rust-shared/data/excel/all_excel_functions.xlsx diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index fa0b38e1a8..c07ea23894 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -7,7 +7,7 @@ use crate::{ cell_values::CellValues, controller::GridController, grid::{file::sheet_schema::export_sheet, CodeCellLanguage, Sheet, SheetId}, - CellValue, Pos, SheetPos, + CellValue, CodeCellValue, Pos, SheetPos, }; use bytes::Bytes; use calamine::{Data as ExcelData, Reader as ExcelReader, Xlsx, XlsxError}; @@ -213,7 +213,7 @@ impl GridController { x: insert_at.x + x as i64, y: insert_at.y + y as i64, }; - let cell_value = CellValue::Code(crate::CodeCellValue { + let cell_value = CellValue::Code(CodeCellValue { language: CodeCellLanguage::Formula, code: cell.to_string(), }); @@ -428,19 +428,22 @@ mod test { let sheet = gc.sheet(sheet_id); assert_eq!( - sheet.cell_value((0, 0).into()), + sheet.cell_value((0, 1).into()), Some(CellValue::Number(1.into())) ); assert_eq!( - sheet.cell_value((2, 9).into()), + sheet.cell_value((2, 10).into()), Some(CellValue::Number(12.into())) ); - assert_eq!(sheet.cell_value((0, 5).into()), None); + assert_eq!(sheet.cell_value((0, 6).into()), None); assert_eq!( - sheet.cell_value((3, 1).into()), - Some(CellValue::Number(3.into())) + sheet.cell_value((3, 2).into()), + Some(CellValue::Code(CodeCellValue { + language: CodeCellLanguage::Formula, + code: "C1:C5".into() + })) ); - assert_eq!(sheet.cell_value((3, 0).into()), None); + assert_eq!(sheet.cell_value((3, 1).into()), None); } #[test] diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index 8f3fb2b8e6..9958148ab6 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -54,9 +54,10 @@ mod tests { use std::io::Read; use crate::{ + grid::{CodeCellLanguage, CodeRunResult}, test_util::{assert_cell_value_row, print_table}, wasm_bindings::js::clear_js_calls, - Rect, + CellValue, Rect, RunErrorMsg, }; use super::*; @@ -68,6 +69,8 @@ mod tests { // const EXCEL_FILE: &str = "../quadratic-rust-shared/data/excel/temperature.xlsx"; const EXCEL_FILE: &str = "../quadratic-rust-shared/data/excel/basic.xlsx"; + const EXCEL_FUNCTIONS_FILE: &str = + "../quadratic-rust-shared/data/excel/all_excel_functions.xlsx"; // const EXCEL_FILE: &str = "../quadratic-rust-shared/data/excel/financial_sample.xlsx"; const PARQUET_FILE: &str = "../quadratic-rust-shared/data/parquet/alltypes_plain.parquet"; // const MEDIUM_PARQUET_FILE: &str = "../quadratic-rust-shared/data/parquet/lineitem.parquet"; @@ -165,7 +168,7 @@ mod tests { let mut buffer = vec![0; metadata.len() as usize]; file.read_exact(&mut buffer).expect("buffer overflow"); - let _ = grid_controller.import_excel(buffer, "temperature.xlsx"); + let _ = grid_controller.import_excel(buffer, "basic.xlsx"); let sheet_id = grid_controller.grid.sheets()[0].id; print_table( @@ -217,6 +220,54 @@ mod tests { ); } + #[test] + fn import_all_excel_functions() { + let mut grid_controller = GridController::test_blank(); + let pos = Pos { x: 0, y: 0 }; + let mut file = File::open(EXCEL_FUNCTIONS_FILE).unwrap(); + let metadata = std::fs::metadata(EXCEL_FUNCTIONS_FILE).expect("unable to read metadata"); + let mut buffer = vec![0; metadata.len() as usize]; + file.read_exact(&mut buffer).expect("buffer overflow"); + + let _ = grid_controller.import_excel(buffer, "all_excel_functions.xlsx"); + let sheet_id = grid_controller.grid.sheets()[0].id; + + print_table( + &grid_controller, + sheet_id, + Rect::new_span(pos, Pos { x: 10, y: 10 }), + ); + + let sheet = grid_controller.grid.try_sheet(sheet_id).unwrap(); + let (y_start, y_end) = sheet.column_bounds(0, true).unwrap(); + assert_eq!(y_start, 1); + assert_eq!(y_end, 512); + for y in y_start..=y_end { + let pos = Pos { x: 0, y }; + // all cells should be formula code cells + let code_cell = sheet.cell_value(pos).unwrap(); + match &code_cell { + CellValue::Code(code_cell_value) => { + assert_eq!(code_cell_value.language, CodeCellLanguage::Formula); + } + _ => panic!("expected code cell"), + } + + // all code cells should have valid function names, + // valid functions may not be implemented yet + let code_run = sheet.code_run(pos).unwrap(); + if code_run.std_err.is_some() { + match &code_run.result { + CodeRunResult::Err(error) => match &error.msg { + RunErrorMsg::BadFunctionName => panic!("expected valid function name"), + _ => (), + }, + _ => (), + } + } + } + } + #[test] fn imports_a_simple_parquet() { let mut grid_controller = GridController::test(); diff --git a/quadratic-core/src/formulas/functions/excel.rs b/quadratic-core/src/formulas/functions/excel.rs index 92e98cf033..7cf2acc9fd 100644 --- a/quadratic-core/src/formulas/functions/excel.rs +++ b/quadratic-core/src/formulas/functions/excel.rs @@ -1,9 +1,20 @@ use std::collections::HashSet; use lazy_static::lazy_static; +use regex::Regex; pub fn is_valid_excel_function(name: &str) -> bool { - ALL_EXCEL_FUNCTIONS.contains(name.to_ascii_uppercase().as_str()) + ALL_EXCEL_FUNCTIONS.contains( + clean_excel_formula_prefix(name) + .to_ascii_uppercase() + .as_str(), + ) +} + +// Remove the _xlfn. _xludf. prefix from the function name. +pub fn clean_excel_formula_prefix(name: &str) -> String { + let re = Regex::new(r"^(_xlfn|_xludf)\.").unwrap(); + re.replace(name, "").into_owned() } lazy_static! { diff --git a/quadratic-core/src/formulas/functions/mod.rs b/quadratic-core/src/formulas/functions/mod.rs index 6502f82758..ad9babc68e 100644 --- a/quadratic-core/src/formulas/functions/mod.rs +++ b/quadratic-core/src/formulas/functions/mod.rs @@ -23,7 +23,11 @@ use crate::{ }; pub fn lookup_function(name: &str) -> Option<&'static FormulaFunction> { - ALL_FUNCTIONS.get(name.to_ascii_uppercase().as_str()) + ALL_FUNCTIONS.get( + excel::clean_excel_formula_prefix(name) + .to_ascii_uppercase() + .as_str(), + ) } pub const CATEGORIES: &[FormulaFunctionCategory] = &[ diff --git a/quadratic-core/src/formulas/lexer.rs b/quadratic-core/src/formulas/lexer.rs index 16def8cbf0..196e0875d8 100644 --- a/quadratic-core/src/formulas/lexer.rs +++ b/quadratic-core/src/formulas/lexer.rs @@ -26,7 +26,8 @@ fn new_fullmatch_regex(s: &str) -> Regex { /// Function call consisting of a letter or underscore followed by any letters, /// digits, and/or underscores terminated with a `(`. -const FUNCTION_CALL_PATTERN: &str = r"[A-Za-z_][A-Za-z_\d]*\("; +/// Can contain a `.` between parts of the function name. +const FUNCTION_CALL_PATTERN: &str = r"[A-Za-z_](\.?[A-Za-z_\d])*\("; /// A1-style cell reference. /// diff --git a/quadratic-rust-shared/data/excel/all_excel_functions.xlsx b/quadratic-rust-shared/data/excel/all_excel_functions.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..746dfafc467d985bc3ad0ef4037341f31adc8df5 GIT binary patch literal 22310 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 Date: Sat, 29 Jun 2024 04:46:42 +0530 Subject: [PATCH 11/14] lint wasm --- quadratic-core/src/controller/user_actions/import.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index 9958148ab6..4bc0d88d7a 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -256,13 +256,9 @@ mod tests { // all code cells should have valid function names, // valid functions may not be implemented yet let code_run = sheet.code_run(pos).unwrap(); - if code_run.std_err.is_some() { - match &code_run.result { - CodeRunResult::Err(error) => match &error.msg { - RunErrorMsg::BadFunctionName => panic!("expected valid function name"), - _ => (), - }, - _ => (), + if let CodeRunResult::Err(error) = &code_run.result { + if error.msg == RunErrorMsg::BadFunctionName { + panic!("expected valid function name") } } } From 7b42bffa967eca22cdf31c479f23adc92db1a21a Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Sat, 29 Jun 2024 16:18:58 +0530 Subject: [PATCH 12/14] clean _xlfn. _xludf. prefix while importing --- quadratic-core/src/controller/operations/import.rs | 7 ++++++- quadratic-core/src/formulas/functions/excel.rs | 13 +------------ quadratic-core/src/formulas/functions/mod.rs | 6 +----- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index c07ea23894..b019fa08e1 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -2,6 +2,7 @@ use std::io::Cursor; use anyhow::{anyhow, bail, Result}; use lexicon_fractional_index::key_between; +use regex::Regex; use crate::{ cell_values::CellValues, @@ -146,6 +147,9 @@ impl GridController { y: row as i64 + 1, }; + // remove the _xlfn. _xludf. prefix from the function name + let re = Regex::new(r"(?:^|[^A-Za-z\d])(?:_xlfn|_xludf)\.").unwrap(); + let mut order = key_between(&None, &None).unwrap_or("A0".to_string()); for sheet_name in sheets { // add the sheet @@ -209,13 +213,14 @@ impl GridController { for (y, row) in formula.rows().enumerate() { for (x, cell) in row.iter().enumerate() { if !cell.is_empty() { + let code = re.replace_all(cell, "").into_owned(); let pos = Pos { x: insert_at.x + x as i64, y: insert_at.y + y as i64, }; let cell_value = CellValue::Code(CodeCellValue { language: CodeCellLanguage::Formula, - code: cell.to_string(), + code, }); sheet.set_cell_value(pos, cell_value); // add code compute operation, to generate code runs diff --git a/quadratic-core/src/formulas/functions/excel.rs b/quadratic-core/src/formulas/functions/excel.rs index 7cf2acc9fd..92e98cf033 100644 --- a/quadratic-core/src/formulas/functions/excel.rs +++ b/quadratic-core/src/formulas/functions/excel.rs @@ -1,20 +1,9 @@ use std::collections::HashSet; use lazy_static::lazy_static; -use regex::Regex; pub fn is_valid_excel_function(name: &str) -> bool { - ALL_EXCEL_FUNCTIONS.contains( - clean_excel_formula_prefix(name) - .to_ascii_uppercase() - .as_str(), - ) -} - -// Remove the _xlfn. _xludf. prefix from the function name. -pub fn clean_excel_formula_prefix(name: &str) -> String { - let re = Regex::new(r"^(_xlfn|_xludf)\.").unwrap(); - re.replace(name, "").into_owned() + ALL_EXCEL_FUNCTIONS.contains(name.to_ascii_uppercase().as_str()) } lazy_static! { diff --git a/quadratic-core/src/formulas/functions/mod.rs b/quadratic-core/src/formulas/functions/mod.rs index ad9babc68e..6502f82758 100644 --- a/quadratic-core/src/formulas/functions/mod.rs +++ b/quadratic-core/src/formulas/functions/mod.rs @@ -23,11 +23,7 @@ use crate::{ }; pub fn lookup_function(name: &str) -> Option<&'static FormulaFunction> { - ALL_FUNCTIONS.get( - excel::clean_excel_formula_prefix(name) - .to_ascii_uppercase() - .as_str(), - ) + ALL_FUNCTIONS.get(name.to_ascii_uppercase().as_str()) } pub const CATEGORIES: &[FormulaFunctionCategory] = &[ From 083769a1cec16f33d9d4f13d5bf71ef940f0c0c4 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Tue, 2 Jul 2024 05:32:04 +0530 Subject: [PATCH 13/14] prefix regex, error, fs::read --- .../src/controller/operations/import.rs | 21 ++++---------- .../src/controller/user_actions/import.rs | 29 ++++--------------- .../src/formulas/functions/excel.rs | 13 ++++++++- quadratic-core/src/formulas/functions/mod.rs | 6 +++- 4 files changed, 28 insertions(+), 41 deletions(-) diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index b019fa08e1..450fa964ea 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -2,7 +2,6 @@ use std::io::Cursor; use anyhow::{anyhow, bail, Result}; use lexicon_fractional_index::key_between; -use regex::Regex; use crate::{ cell_values::CellValues, @@ -131,12 +130,10 @@ impl GridController { file_name: &str, ) -> Result> { let mut ops = vec![] as Vec; - let error = - |message: String| anyhow!("Error parsing Excel file {}: {}", file_name, message); + let error = |e: XlsxError| anyhow!("Error parsing Excel file {file_name}: {e}"); let cursor = Cursor::new(file); - let mut workbook: Xlsx<_> = - ExcelReader::new(cursor).map_err(|e: XlsxError| error(e.to_string()))?; + let mut workbook: Xlsx<_> = ExcelReader::new(cursor).map_err(error)?; let sheets = workbook.sheet_names().to_owned(); // first cell in excel is A1, but first cell in quadratic is A0 @@ -147,9 +144,6 @@ impl GridController { y: row as i64 + 1, }; - // remove the _xlfn. _xludf. prefix from the function name - let re = Regex::new(r"(?:^|[^A-Za-z\d])(?:_xlfn|_xludf)\.").unwrap(); - let mut order = key_between(&None, &None).unwrap_or("A0".to_string()); for sheet_name in sheets { // add the sheet @@ -157,9 +151,7 @@ impl GridController { order = key_between(&Some(order), &None).unwrap_or("A0".to_string()); // values - let range = workbook - .worksheet_range(&sheet_name) - .map_err(|e: XlsxError| error(e.to_string()))?; + let range = workbook.worksheet_range(&sheet_name).map_err(error)?; let insert_at = range.start().map_or_else(Pos::default, xlsx_range_to_pos); for (y, row) in range.rows().enumerate() { for (x, cell) in row.iter().enumerate() { @@ -205,22 +197,19 @@ impl GridController { } // formulas - let formula = workbook - .worksheet_formula(&sheet_name) - .map_err(|e: XlsxError| error(e.to_string()))?; + let formula = workbook.worksheet_formula(&sheet_name).map_err(error)?; let insert_at = formula.start().map_or_else(Pos::default, xlsx_range_to_pos); let mut formula_compute_ops = vec![]; for (y, row) in formula.rows().enumerate() { for (x, cell) in row.iter().enumerate() { if !cell.is_empty() { - let code = re.replace_all(cell, "").into_owned(); let pos = Pos { x: insert_at.x + x as i64, y: insert_at.y + y as i64, }; let cell_value = CellValue::Code(CodeCellValue { language: CodeCellLanguage::Formula, - code, + code: cell.to_string(), }); sheet.set_cell_value(pos, cell_value); // add code compute operation, to generate code runs diff --git a/quadratic-core/src/controller/user_actions/import.rs b/quadratic-core/src/controller/user_actions/import.rs index 4bc0d88d7a..96499bc086 100644 --- a/quadratic-core/src/controller/user_actions/import.rs +++ b/quadratic-core/src/controller/user_actions/import.rs @@ -49,10 +49,6 @@ impl GridController { #[cfg(test)] mod tests { - - use std::fs::File; - use std::io::Read; - use crate::{ grid::{CodeCellLanguage, CodeRunResult}, test_util::{assert_cell_value_row, print_table}, @@ -163,12 +159,8 @@ mod tests { fn imports_a_simple_excel_file() { let mut grid_controller = GridController::test_blank(); let pos = Pos { x: 0, y: 0 }; - let mut file = File::open(EXCEL_FILE).unwrap(); - let metadata = std::fs::metadata(EXCEL_FILE).expect("unable to read metadata"); - let mut buffer = vec![0; metadata.len() as usize]; - file.read_exact(&mut buffer).expect("buffer overflow"); - - let _ = grid_controller.import_excel(buffer, "basic.xlsx"); + let file: Vec = std::fs::read(EXCEL_FILE).expect("Failed to read file"); + let _ = grid_controller.import_excel(file, "basic.xlsx"); let sheet_id = grid_controller.grid.sheets()[0].id; print_table( @@ -224,12 +216,8 @@ mod tests { fn import_all_excel_functions() { let mut grid_controller = GridController::test_blank(); let pos = Pos { x: 0, y: 0 }; - let mut file = File::open(EXCEL_FUNCTIONS_FILE).unwrap(); - let metadata = std::fs::metadata(EXCEL_FUNCTIONS_FILE).expect("unable to read metadata"); - let mut buffer = vec![0; metadata.len() as usize]; - file.read_exact(&mut buffer).expect("buffer overflow"); - - let _ = grid_controller.import_excel(buffer, "all_excel_functions.xlsx"); + let file: Vec = std::fs::read(EXCEL_FUNCTIONS_FILE).expect("Failed to read file"); + let _ = grid_controller.import_excel(file, "all_excel_functions.xlsx"); let sheet_id = grid_controller.grid.sheets()[0].id; print_table( @@ -269,13 +257,8 @@ mod tests { let mut grid_controller = GridController::test(); let sheet_id = grid_controller.grid.sheets()[0].id; let pos = Pos { x: 0, y: 0 }; - let mut file = File::open(PARQUET_FILE).unwrap(); - let metadata = std::fs::metadata(PARQUET_FILE).expect("unable to read metadata"); - let mut buffer = vec![0; metadata.len() as usize]; - file.read_exact(&mut buffer).expect("buffer overflow"); - - let _ = - grid_controller.import_parquet(sheet_id, buffer, "alltypes_plain.parquet", pos, None); + let file: Vec = std::fs::read(PARQUET_FILE).expect("Failed to read file"); + let _ = grid_controller.import_parquet(sheet_id, file, "alltypes_plain.parquet", pos, None); print_table( &grid_controller, diff --git a/quadratic-core/src/formulas/functions/excel.rs b/quadratic-core/src/formulas/functions/excel.rs index 92e98cf033..932d5a5785 100644 --- a/quadratic-core/src/formulas/functions/excel.rs +++ b/quadratic-core/src/formulas/functions/excel.rs @@ -3,7 +3,15 @@ use std::collections::HashSet; use lazy_static::lazy_static; pub fn is_valid_excel_function(name: &str) -> bool { - ALL_EXCEL_FUNCTIONS.contains(name.to_ascii_uppercase().as_str()) + ALL_EXCEL_FUNCTIONS.contains( + remove_excel_function_prefix(name) + .to_ascii_uppercase() + .as_str(), + ) +} + +pub fn remove_excel_function_prefix(name: &str) -> String { + PREFIX_RE.replace(name, "").to_string() } lazy_static! { @@ -11,6 +19,9 @@ lazy_static! { static ref ALL_EXCEL_FUNCTIONS: HashSet<&'static str> = { EXCEL_FUNCTIONS_LIST.iter().cloned().collect::>() }; + + // regex to remove _xlfn. _xludf. prefix from the function name + static ref PREFIX_RE: regex::Regex = regex::Regex::new(r"^_xl(?:fn|udf)\.").unwrap(); } const EXCEL_FUNCTIONS_LIST: [&str; 512] = [ diff --git a/quadratic-core/src/formulas/functions/mod.rs b/quadratic-core/src/formulas/functions/mod.rs index 6502f82758..3e5f17c0ed 100644 --- a/quadratic-core/src/formulas/functions/mod.rs +++ b/quadratic-core/src/formulas/functions/mod.rs @@ -23,7 +23,11 @@ use crate::{ }; pub fn lookup_function(name: &str) -> Option<&'static FormulaFunction> { - ALL_FUNCTIONS.get(name.to_ascii_uppercase().as_str()) + ALL_FUNCTIONS.get( + excel::remove_excel_function_prefix(name) + .to_ascii_uppercase() + .as_str(), + ) } pub const CATEGORIES: &[FormulaFunctionCategory] = &[ From 40f4a79fc16514411b354209e2099cfa94b299ed Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Tue, 2 Jul 2024 05:52:19 +0530 Subject: [PATCH 14/14] line_number --- quadratic-core/src/controller/execution/run_code/mod.rs | 8 +++++--- quadratic-core/src/span.rs | 4 ++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/quadratic-core/src/controller/execution/run_code/mod.rs b/quadratic-core/src/controller/execution/run_code/mod.rs index db60207e7a..98456c2198 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -265,10 +265,10 @@ impl GridController { // cell may have been deleted before the async operation completed return Ok(()); }; - if !matches!(code_cell, CellValue::Code(_)) { + let CellValue::Code(code_cell_value) = code_cell else { // code may have been replaced while waiting for async operation return Ok(()); - } + }; let result = CodeRunResult::Err(error.clone()); @@ -293,7 +293,9 @@ impl GridController { formatted_code_string: None, result, return_type: None, - line_number: error.span.map(|span| span.start), + line_number: error + .span + .map(|span| span.line_number_of_str(&code_cell_value.code) as u32), output_type: None, std_out: None, std_err: Some(error.msg.to_string()), diff --git a/quadratic-core/src/span.rs b/quadratic-core/src/span.rs index 27946cebab..f756a9b6ae 100644 --- a/quadratic-core/src/span.rs +++ b/quadratic-core/src/span.rs @@ -37,6 +37,10 @@ impl Span { let range: Range = self.into(); &s[range] } + /// Returns the line number of the start of the span + pub fn line_number_of_str(self, s: &str) -> usize { + s[..self.start as usize].matches('\n').count() + 1 + } } impl From> for Span { fn from(spanned: Spanned) -> Self {