diff --git a/quadratic-client/src/app/gridGL/cells/CellsArray.ts b/quadratic-client/src/app/gridGL/cells/CellsArray.ts index c132d4b7ff..8295f2bbfa 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.error('[CellsArray] Error parsing codeCell.evaluation_result', error); + } + } } }; 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 fb26140d84..eb8840c088 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/controller/execution/execute_operation/execute_code.rs b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs index 7bf284723d..d3c8f9f314 100644 --- a/quadratic-core/src/controller/execution/execute_operation/execute_code.rs +++ b/quadratic-core/src/controller/execution/execute_operation/execute_code.rs @@ -97,8 +97,9 @@ impl GridController { op: Operation, ) { if let Operation::ComputeCode { sheet_pos } = op { - if !transaction.is_user() { - unreachable!("Only a user transaction should have a ComputeCode"); + if !transaction.is_user() && !transaction.is_server() { + 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/execution/run_code/get_cells.rs b/quadratic-core/src/controller/execution/run_code/get_cells.rs index b3d3a03661..3ae11d4dca 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}; @@ -52,12 +52,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 3847676c5a..98456c2198 100644 --- a/quadratic-core/src/controller/execution/run_code/mod.rs +++ b/quadratic-core/src/controller/execution/run_code/mod.rs @@ -242,8 +242,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, @@ -266,18 +265,12 @@ 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 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) => { @@ -288,7 +281,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(), @@ -300,10 +293,12 @@ impl GridController { formatted_code_string: None, result, return_type: None, - line_number, + 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_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); } } } diff --git a/quadratic-core/src/controller/operations/import.rs b/quadratic-core/src/controller/operations/import.rs index 8e5ce2dbd9..450fa964ea 100644 --- a/quadratic-core/src/controller/operations/import.rs +++ b/quadratic-core/src/controller/operations/import.rs @@ -6,8 +6,8 @@ use lexicon_fractional_index::key_between; use crate::{ cell_values::CellValues, controller::GridController, - grid::{file::sheet_schema::export_sheet, Sheet, SheetId}, - CellValue, Pos, SheetPos, + grid::{file::sheet_schema::export_sheet, CodeCellLanguage, Sheet, SheetId}, + CellValue, CodeCellValue, Pos, SheetPos, }; use bytes::Bytes; use calamine::{Data as ExcelData, Reader as ExcelReader, Xlsx, XlsxError}; @@ -130,28 +130,32 @@ impl GridController { file_name: &str, ) -> Result> { let mut ops = vec![] as Vec; - let insert_at = Pos::default(); - 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 + // 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, + y: row as i64 + 1, + }; + let mut order = key_between(&None, &None).unwrap_or("A0".to_string()); for sheet_name in sheets { // add the sheet let mut sheet = Sheet::new(SheetId::new(), sheet_name.to_owned(), order.clone()); order = key_between(&Some(order), &None).unwrap_or("A0".to_string()); - let range = workbook - .worksheet_range(&sheet_name) - .map_err(|e: XlsxError| error(e.to_string()))?; - + // values + 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, col) in row.iter().enumerate() { - let cell_value = match col { + for (x, cell) in row.iter().enumerate() { + let cell_value = match cell { ExcelData::Empty => continue, ExcelData::String(value) => CellValue::Text(value.to_string()), ExcelData::DateTimeIso(ref value) => CellValue::Text(value.to_string()), @@ -191,12 +195,36 @@ impl GridController { ); } } + + // formulas + 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 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(), + }); + sheet.set_cell_value(pos, cell_value); + // add code compute operation, to generate code runs + formula_compute_ops.push(Operation::ComputeCode { + sheet_pos: pos.to_sheet_pos(sheet.id), + }); + } + } + } // add new sheets ops.push(Operation::AddSheetSchema { schema: export_sheet(&sheet), }); + ops.extend(formula_compute_ops); } - Ok(ops) } @@ -394,19 +422,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 124579a611..96499bc086 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(()) } @@ -44,14 +49,11 @@ 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}, wasm_bindings::js::clear_js_calls, - Rect, + CellValue, Rect, RunErrorMsg, }; use super::*; @@ -63,6 +65,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"; @@ -155,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, "temperature.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( @@ -174,7 +174,7 @@ mod tests { sheet_id, 0, 10, - 0, + 1, vec![ "Empty", "String", @@ -195,7 +195,7 @@ mod tests { sheet_id, 0, 10, - 1, + 2, vec![ "", "Hello", @@ -212,18 +212,53 @@ mod tests { ); } + #[test] + fn import_all_excel_functions() { + let mut grid_controller = GridController::test_blank(); + let pos = Pos { x: 0, y: 0 }; + 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( + &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 let CodeRunResult::Err(error) = &code_run.result { + if error.msg == RunErrorMsg::BadFunctionName { + panic!("expected valid function name") + } + } + } + } + #[test] fn imports_a_simple_parquet() { 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/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/formulas/functions/excel.rs b/quadratic-core/src/formulas/functions/excel.rs new file mode 100644 index 0000000000..932d5a5785 --- /dev/null +++ b/quadratic-core/src/formulas/functions/excel.rs @@ -0,0 +1,540 @@ +use std::collections::HashSet; + +use lazy_static::lazy_static; + +pub fn is_valid_excel_function(name: &str) -> bool { + 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! { + /// Set containing all excel functions. + 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] = [ + "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..3e5f17c0ed 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; @@ -22,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] = &[ 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-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), 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 { 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 0000000000..746dfafc46 Binary files /dev/null and b/quadratic-rust-shared/data/excel/all_excel_functions.xlsx differ