From 4a5db81cd68d8085ab87e119c3b890ab36170f94 Mon Sep 17 00:00:00 2001 From: HactarCE <6060305+HactarCE@users.noreply.github.com> Date: Fri, 3 Mar 2023 16:31:55 -0500 Subject: [PATCH 1/3] Add array literals in formulas --- quadratic-core/src/formulas/ast.rs | 20 +++++++ quadratic-core/src/formulas/errors.rs | 4 ++ quadratic-core/src/formulas/lexer.rs | 5 +- .../src/formulas/parser/rules/expression.rs | 56 ++++++++++++++++++- quadratic-core/src/formulas/tests.rs | 47 +++++++++++++++- quadratic-core/src/formulas/value.rs | 9 ++- 6 files changed, 135 insertions(+), 6 deletions(-) diff --git a/quadratic-core/src/formulas/ast.rs b/quadratic-core/src/formulas/ast.rs index 69ed7feeff..25881b02ac 100644 --- a/quadratic-core/src/formulas/ast.rs +++ b/quadratic-core/src/formulas/ast.rs @@ -1,4 +1,5 @@ use futures::future::{FutureExt, LocalBoxFuture}; +use itertools::Itertools; use serde::{Deserialize, Serialize}; use smallvec::smallvec; use std::fmt; @@ -25,6 +26,7 @@ pub enum AstNodeContents { args: Vec, }, Paren(Box), + Array(Vec>), CellRef(CellRef), String(String), Number(f64), @@ -44,6 +46,11 @@ impl fmt::Display for AstNodeContents { Ok(()) } AstNodeContents::Paren(contents) => write!(f, "({contents})"), + AstNodeContents::Array(a) => write!( + f, + "{{{}}}", + a.iter().map(|row| row.iter().join(", ")).join("; "), + ), AstNodeContents::CellRef(cellref) => write!(f, "{cellref}"), AstNodeContents::String(s) => write!(f, "{s:?}"), AstNodeContents::Number(n) => write!(f, "{n:?}"), @@ -59,6 +66,7 @@ impl AstNodeContents { _ => "expression", }, AstNodeContents::Paren(contents) => contents.inner.type_string(), + AstNodeContents::Array(_) => "array literal", AstNodeContents::CellRef(_) => "cell reference", AstNodeContents::String(_) => "string literal", AstNodeContents::Number(_) => "numeric literal", @@ -159,6 +167,18 @@ impl AstNode { AstNodeContents::Paren(expr) => expr.eval(grid, pos).await?.inner, + AstNodeContents::Array(a) => { + let mut array_of_values = vec![]; + for row in a { + let mut row_of_values = smallvec![]; + for elem_expr in row { + row_of_values.push(elem_expr.eval(grid, pos).await?.inner); + } + array_of_values.push(row_of_values); + } + Value::Array(array_of_values) + } + AstNodeContents::CellRef(cell_ref) => self.get_cell(grid, pos, *cell_ref).await?, AstNodeContents::String(s) => Value::String(s.clone()), diff --git a/quadratic-core/src/formulas/errors.rs b/quadratic-core/src/formulas/errors.rs index 550ca6c552..d69b1dcd74 100644 --- a/quadratic-core/src/formulas/errors.rs +++ b/quadratic-core/src/formulas/errors.rs @@ -51,6 +51,7 @@ pub enum FormulaErrorMsg { expected: (usize, usize), got: (usize, usize), }, + NonRectangularArray, BadArgumentCount, BadFunctionName, BadCellReference, @@ -86,6 +87,9 @@ impl fmt::Display for FormulaErrorMsg { Self::ArraySizeMismatch { expected, got } => { write!(f, "Array size mismatch: expected {expected:?}, got {got:?}") } + Self::NonRectangularArray => { + write!(f, "Array must be rectangular") + } Self::BadArgumentCount => { // TODO: give a nicer error message that says what the arguments // should be diff --git a/quadratic-core/src/formulas/lexer.rs b/quadratic-core/src/formulas/lexer.rs index bdd58e558d..b4d21fc752 100644 --- a/quadratic-core/src/formulas/lexer.rs +++ b/quadratic-core/src/formulas/lexer.rs @@ -130,8 +130,10 @@ pub enum Token { RBrace, // Separators - #[strum(to_string = "argument separator")] + #[strum(to_string = "argument separator (comma)")] ArgSep, + #[strum(to_string = "array row seperator (semicolon)")] + RowSep, // Comparison operators #[strum(to_string = "equals comparison")] @@ -211,6 +213,7 @@ impl Token { "]" => Self::RBracket, "}" => Self::RBrace, "," => Self::ArgSep, + ";" => Self::RowSep, "=" | "==" => Self::Eql, "<>" | "!=" => Self::Neq, "<" => Self::Lt, diff --git a/quadratic-core/src/formulas/parser/rules/expression.rs b/quadratic-core/src/formulas/parser/rules/expression.rs index d971bcd23a..6b4d422327 100644 --- a/quadratic-core/src/formulas/parser/rules/expression.rs +++ b/quadratic-core/src/formulas/parser/rules/expression.rs @@ -117,11 +117,12 @@ impl SyntaxRule for ExpressionWithPrecedence { // just match all of them. match t { Token::LParen => true, + Token::LBracket => false, + Token::LBrace => true, - Token::LBracket | Token::LBrace => false, Token::RParen | Token::RBracket | Token::RBrace => false, - Token::ArgSep => false, + Token::ArgSep | Token::RowSep => false, Token::Eql | Token::Neq | Token::Lt | Token::Gt | Token::Lte | Token::Gte => false, @@ -162,6 +163,7 @@ impl SyntaxRule for ExpressionWithPrecedence { FunctionCall.map(Some), StringLiteral.map(Some), NumericLiteral.map(Some), + ArrayLiteral.map(Some), CellReference.map(Some), ParenExpression.map(Some), Epsilon.map(|_| None), @@ -357,3 +359,53 @@ impl SyntaxRule for ParenExpression { )) } } + +/// Matches an array literal. +pub struct ArrayLiteral; +impl_display!(for ArrayLiteral, "array literal, such as '{{1, 2; 3, 4}}'"); +impl SyntaxRule for ArrayLiteral { + type Output = AstNode; + + fn prefix_matches(&self, mut p: Parser<'_>) -> bool { + p.next() == Some(Token::LBrace) + } + + fn consume_match(&self, p: &mut Parser<'_>) -> FormulaResult { + let start_span = p.peek_next_span(); + + let mut rows = vec![vec![]]; + p.parse(Token::LBrace)?; + loop { + rows.last_mut().unwrap().push(p.parse(Expression)?); + match p.next() { + Some(Token::ArgSep) => (), // next cell within row + Some(Token::RowSep) => rows.push(vec![]), // start a new row + Some(Token::RBrace) => break, // end of array + _ => { + return Err(p.expected_err(crate::util::join_with_conjunction( + "or", + &[ + Token::ArgSep.to_string(), + Token::RowSep.to_string(), + Token::RBrace.to_string(), + ], + ))) + } + } + } + if rows.last().unwrap().is_empty() && rows.len() > 1 { + rows.pop(); + } + + let end_span = p.span(); + + if !rows.iter().map(|row| row.len()).all_equal() { + return Err(FormulaErrorMsg::NonRectangularArray.with_span(end_span)); + } + + Ok(Spanned { + span: Span::merge(start_span, end_span), + inner: ast::AstNodeContents::Array(rows), + }) + } +} diff --git a/quadratic-core/src/formulas/tests.rs b/quadratic-core/src/formulas/tests.rs index de582726a1..6cfb45efd1 100644 --- a/quadratic-core/src/formulas/tests.rs +++ b/quadratic-core/src/formulas/tests.rs @@ -200,6 +200,50 @@ fn test_formula_array_op() { ); } +#[test] +fn test_array_parsing() { + let f = |x| Value::Number(x as f64); + assert_eq!( + Value::Array(vec![ + smallvec![f(11), f(12)], + smallvec![f(21), f(22)], + smallvec![f(31), f(32)], + ]), + eval(&mut PanicGridMock, "{11, 12; 21, 22; 31, 32}").unwrap(), + ); + + // Test stringification + assert_eq!( + "{11, 12; 21, 22; 31, 32}", + eval_to_string(&mut PanicGridMock, "{11, 12 ;21, 22;31,32}"), + ); + + // Single row + assert_eq!( + "{11, 12, 13}", + eval_to_string(&mut PanicGridMock, "{11, 12, 13}"), + ); + + // Single column + assert_eq!( + "{11; 12; 13}", + eval_to_string(&mut PanicGridMock, "{11; 12; 13}"), + ); + + // Mismatched rows + assert_eq!( + FormulaErrorMsg::NonRectangularArray, + eval(&mut PanicGridMock, "{1; 3, 4}").unwrap_err().msg, + ); + + // Empty array + assert!(eval(&mut PanicGridMock, "{}").is_err()); + assert!(eval(&mut PanicGridMock, "{ }").is_err()); + + // Empty row + assert!(eval(&mut PanicGridMock, "{ ; }").is_err()); +} + #[test] fn test_leading_equals() { assert_eq!("7", eval_to_string(&mut PanicGridMock, "=3+4")); @@ -218,8 +262,7 @@ fn eval_to_string(grid: &mut impl GridProxy, s: &str) -> String { eval(grid, s).unwrap().to_string() } fn eval(grid: &mut impl GridProxy, s: &str) -> FormulaResult { - parse_formula(s, Pos::ORIGIN) - .unwrap() + parse_formula(s, Pos::ORIGIN)? .eval_blocking(grid, Pos::ORIGIN) .map(|value| value.inner) } diff --git a/quadratic-core/src/formulas/value.rs b/quadratic-core/src/formulas/value.rs index 1bbbaa5835..3c64b3775b 100644 --- a/quadratic-core/src/formulas/value.rs +++ b/quadratic-core/src/formulas/value.rs @@ -1,3 +1,4 @@ +use itertools::Itertools; use smallvec::{smallvec, SmallVec}; use std::fmt; @@ -26,7 +27,13 @@ impl fmt::Display for Value { Value::Number(n) => write!(f, "{n}"), Value::Bool(true) => write!(f, "TRUE"), Value::Bool(false) => write!(f, "FALSE"), - Value::Array(_) => write!(f, "[array]"), + Value::Array(rows) => { + write!( + f, + "{{{}}}", + rows.iter().map(|row| row.iter().join(", ")).join("; "), + ) + } Value::MissingErr => write!(f, "[missing]"), } } From 21103c91412c2ca6a6061693d11fe3bd93950c71 Mon Sep 17 00:00:00 2001 From: HactarCE <6060305+HactarCE@users.noreply.github.com> Date: Fri, 3 Mar 2023 17:07:36 -0500 Subject: [PATCH 2/3] Bump quadratic-core version --- quadratic-core/Cargo.lock | 2 +- quadratic-core/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/quadratic-core/Cargo.lock b/quadratic-core/Cargo.lock index f1a97a0db2..aa71de21ea 100644 --- a/quadratic-core/Cargo.lock +++ b/quadratic-core/Cargo.lock @@ -384,7 +384,7 @@ dependencies = [ [[package]] name = "quadratic-core" -version = "0.1.1" +version = "0.1.2" dependencies = [ "anyhow", "async-trait", diff --git a/quadratic-core/Cargo.toml b/quadratic-core/Cargo.toml index ef15d77f0c..9392f9efcb 100644 --- a/quadratic-core/Cargo.toml +++ b/quadratic-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "quadratic-core" -version = "0.1.1" +version = "0.1.2" authors = ["Andrew Farkas "] edition = "2021" description = "Infinite data grid with Python, JavaScript, and SQL built-in" From fdb8f3b6ce418ff89378995c596082e2cbd80f87 Mon Sep 17 00:00:00 2001 From: HactarCE <6060305+HactarCE@users.noreply.github.com> Date: Fri, 3 Mar 2023 17:07:42 -0500 Subject: [PATCH 3/3] Update formula docs URL --- src/constants/urls.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/urls.ts b/src/constants/urls.ts index 560c3969a2..7d5680c41d 100644 --- a/src/constants/urls.ts +++ b/src/constants/urls.ts @@ -1,5 +1,5 @@ export const API_URL = 'http://localhost:8000'; export const DOCUMENTATION_URL = 'https://docs.quadratichq.com'; export const DOCUMENTATION_PYTHON_URL = `${DOCUMENTATION_URL}/reference/python-cell-reference`; -export const DOCUMENTATION_FORMULAS_URL = `${DOCUMENTATION_URL}`; // TODO +export const DOCUMENTATION_FORMULAS_URL = `${DOCUMENTATION_URL}/formulas`; export const BUG_REPORT_URL = 'https://github.com/quadratichq/quadratic/issues';