Skip to content

Commit

Permalink
Merge pull request #336 from quadratichq/ajf/formula-array-literals
Browse files Browse the repository at this point in the history
Add array literals in formulas
  • Loading branch information
davidkircos authored Mar 5, 2023
2 parents 6a57136 + fdb8f3b commit 51dd41a
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 9 deletions.
2 changes: 1 addition & 1 deletion quadratic-core/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion quadratic-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "quadratic-core"
version = "0.1.1"
version = "0.1.2"
authors = ["Andrew Farkas <andrew.farkas@quadratic.to>"]
edition = "2021"
description = "Infinite data grid with Python, JavaScript, and SQL built-in"
Expand Down
20 changes: 20 additions & 0 deletions quadratic-core/src/formulas/ast.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use futures::future::{FutureExt, LocalBoxFuture};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use smallvec::smallvec;
use std::fmt;
Expand All @@ -25,6 +26,7 @@ pub enum AstNodeContents {
args: Vec<AstNode>,
},
Paren(Box<AstNode>),
Array(Vec<Vec<AstNode>>),
CellRef(CellRef),
String(String),
Number(f64),
Expand All @@ -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:?}"),
Expand All @@ -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",
Expand Down Expand Up @@ -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()),
Expand Down
4 changes: 4 additions & 0 deletions quadratic-core/src/formulas/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ pub enum FormulaErrorMsg {
expected: (usize, usize),
got: (usize, usize),
},
NonRectangularArray,
BadArgumentCount,
BadFunctionName,
BadCellReference,
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion quadratic-core/src/formulas/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -211,6 +213,7 @@ impl Token {
"]" => Self::RBracket,
"}" => Self::RBrace,
"," => Self::ArgSep,
";" => Self::RowSep,
"=" | "==" => Self::Eql,
"<>" | "!=" => Self::Neq,
"<" => Self::Lt,
Expand Down
56 changes: 54 additions & 2 deletions quadratic-core/src/formulas/parser/rules/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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<Self::Output> {
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),
})
}
}
47 changes: 45 additions & 2 deletions quadratic-core/src/formulas/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand All @@ -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<Value> {
parse_formula(s, Pos::ORIGIN)
.unwrap()
parse_formula(s, Pos::ORIGIN)?
.eval_blocking(grid, Pos::ORIGIN)
.map(|value| value.inner)
}
9 changes: 8 additions & 1 deletion quadratic-core/src/formulas/value.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use itertools::Itertools;
use smallvec::{smallvec, SmallVec};
use std::fmt;

Expand Down Expand Up @@ -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]"),
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/constants/urls.ts
Original file line number Diff line number Diff line change
@@ -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';

0 comments on commit 51dd41a

Please sign in to comment.