Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Formula improvements #335

Merged
merged 5 commits into from
Mar 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.0"
version = "0.1.1"
authors = ["Andrew Farkas <andrew.farkas@quadratic.to>"]
edition = "2021"
description = "Infinite data grid with Python, JavaScript, and SQL built-in"
Expand Down
64 changes: 31 additions & 33 deletions quadratic-core/src/formulas/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,33 +80,6 @@ impl Spanned<AstNodeContents> {
}

impl Formula {
/// Returns a formula that sums some cells.
///
/// TODO: remove this. it's just here for testing
pub fn new_sum(cells: &[Pos]) -> Self {
Self {
ast: AstNode {
span: Span::empty(0),
inner: AstNodeContents::FunctionCall {
func: Spanned {
span: Span::empty(0),
inner: "SUM".to_string(),
},
args: cells
.iter()
.map(|&Pos { x, y }| AstNode {
span: Span::empty(0),
inner: AstNodeContents::CellRef(CellRef {
x: CellRefCoord::Absolute(x),
y: CellRefCoord::Absolute(y),
}),
})
.collect(),
},
},
}
}

/// Evaluates a formula, blocking on async calls.
///
/// Use this when the grid proxy isn't actually doing anything async.
Expand Down Expand Up @@ -170,12 +143,17 @@ impl AstNode {
for arg in args {
arg_values.push(arg.eval(grid, pos).await?);
}
match functions::function_from_name(&func.inner) {
Some(f) => f(Spanned {
span: self.span,
inner: arg_values,
})?,
None => return Err(FormulaErrorMsg::BadFunctionName.with_span(func.span)),
let spanned_arg_values = Spanned {
span: self.span,
inner: arg_values,
};

match func.inner.to_ascii_lowercase().as_str() {
"cell" | "c" => self.array_mapped_get_cell(grid, pos, spanned_arg_values)?,
_ => match functions::pure_function_from_name(&func.inner) {
Some(f) => f(spanned_arg_values)?,
None => return Err(FormulaErrorMsg::BadFunctionName.with_span(func.span)),
},
}
}

Expand Down Expand Up @@ -208,4 +186,24 @@ impl AstNode {
}
Ok(Value::String(grid.get(ref_pos).await.unwrap_or_default()))
}

/// Fetches the contents of the cell at `(x, y)`, but fetches an array of cells
/// if either `x` or `y` is an array.
fn array_mapped_get_cell(
&self,
grid: &mut impl GridProxy,
base_pos: Pos,
args: Spanned<Vec<Spanned<Value>>>,
) -> FormulaResult<Value> {
functions::array_map(args, move |[x, y]| {
let pos = Pos {
x: x.to_integer()?,
y: y.to_integer()?,
};
// Can't have this be async because it needs to mutate `grid` and
// Rust isn't happy about moving a mutable reference to `grid` into
// the closure.
pollster::block_on(self.get_cell(grid, base_pos, CellRef::absolute(pos)))
})
}
}
86 changes: 55 additions & 31 deletions quadratic-core/src/formulas/functions.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use itertools::Itertools;
use smallvec::SmallVec;

use super::*;

Expand All @@ -17,11 +18,11 @@ macro_rules! constant_function {
/// Constructs a thunk that calls `array_mapped()`.
macro_rules! array_mapped {
($closure:expr) => {
|args| array_mapped(args.inner, $closure)
|args| array_map(args, $closure)
};
}

pub fn function_from_name(
pub fn pure_function_from_name(
s: &str,
) -> Option<fn(Spanned<Vec<Spanned<Value>>>) -> FormulaResult<Value>> {
// When adding new functions, also update the code editor completions list.
Expand All @@ -38,8 +39,18 @@ pub fn function_from_name(

// Mathematical operators
"sum" => |args| sum(&args.inner).map(Value::Number),
"+" => array_mapped!(|[a, b]| Ok(Value::Number(a.to_number()? + b.to_number()?))),
"-" => array_mapped!(|[a, b]| Ok(Value::Number(a.to_number()? - b.to_number()?))),
"+" => |args| match args.inner.len() {
1 => array_map(args, |[a]| Ok(Value::Number(a.to_number()?))),
_ => array_map(args, |[a, b]| {
Ok(Value::Number(a.to_number()? + b.to_number()?))
}),
},
"-" => |args| match args.inner.len() {
1 => array_map(args, |[a]| Ok(Value::Number(-a.to_number()?))),
_ => array_map(args, |[a, b]| {
Ok(Value::Number(a.to_number()? - b.to_number()?))
}),
},
"product" => |args| product(&args.inner).map(Value::Number),
"*" => array_mapped!(|[a, b]| Ok(Value::Number(a.to_number()? * b.to_number()?))),
"/" => array_mapped!(|[a, b]| Ok(Value::Number(a.to_number()? / b.to_number()?))),
Expand Down Expand Up @@ -131,16 +142,48 @@ fn flat_iter_strings<'a>(
args.iter().map(|v| v.to_strings()).flatten_ok()
}

/// Produces a function that takes a fixed argument count and can be mapped over
/// arrays.
fn array_mapped<const N: usize>(
args: Vec<Spanned<Value>>,
op: fn([Spanned<Value>; N]) -> FormulaResult<Value>,
/// Maps a fixed-argument-count function over arguments that may be arrays.
pub fn array_map<const N: usize>(
args: Spanned<Vec<Spanned<Value>>>,
mut op: impl FnMut([Spanned<Value>; N]) -> FormulaResult<Value>,
) -> FormulaResult<Value> {
let (args, array_size) = args_with_common_array_size(args)?;
match array_size {
// Compute the results. If any argument is not an array, pretend it's an
// array of one element repeated with the right size.
Some((rows, cols)) => {
let mut output_array = Vec::with_capacity(rows);
for row in 0..rows {
let mut output_row = SmallVec::with_capacity(cols);
for col in 0..cols {
let output_value = op(args
.iter()
.map(|arg| arg.get_array_value(row, col))
.collect::<FormulaResult<Vec<_>>>()?
.try_into()
.unwrap())?;
output_row.push(output_value);
}
output_array.push(output_row);
}
Ok(Value::Array(output_array))
}

// No operands are arrays, so just do the operation once.
None => op(args),
}
}

/// Returns the common `(rows, cols)` of several arguments, or `None` if no
/// arguments are arrays.
pub fn args_with_common_array_size<const N: usize>(
args: Spanned<Vec<Spanned<Value>>>,
) -> FormulaResult<([Spanned<Value>; N], Option<(usize, usize)>)> {
// Check argument count.
let args: [Spanned<Value>; N] = args
.inner
.try_into()
.map_err(|_| FormulaErrorMsg::BadArgumentCount)?;
.map_err(|_| FormulaErrorMsg::BadArgumentCount.with_span(args.span))?;

let mut array_sizes_iter = args
.iter()
Expand All @@ -158,27 +201,8 @@ fn array_mapped<const N: usize>(
}
}

// Compute the results. If any argument is not an array, pretend it's an
// array of one element repeated with the right size.
let (rows, cols) = array_size;
Ok(Value::Array(
(0..rows)
.map(|row| {
(0..cols)
.map(|col| {
op(args
.iter()
.map(|arg| arg.get_array_value(row, col))
.collect::<FormulaResult<Vec<_>>>()?
.try_into()
.unwrap())
})
.try_collect()
})
.try_collect()?,
))
Ok((args, Some(array_size)))
} else {
// No operands are arrays, so just do the operation once.
op(args)
Ok((args, None))
}
}
23 changes: 11 additions & 12 deletions quadratic-core/src/formulas/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,18 @@ const FUNCTION_CALL_PATTERN: &str = r#"[A-Za-z_][A-Za-z_\d]*\("#;
/// \d+ digits
const A1_CELL_REFERENCE_PATTERN: &str = r#"\$?[A-Z]+\$?n?\d+"#;

/// Floating-point or integer number.
/// Floating-point or integer number, without leading sign.
///
/// [+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?
/// [+-]? optional sign
/// ( | ) EITHER
/// \d+ integer part
/// (\.\d*)? with an optional decimal
/// ( | ) OR
/// \.\d+ decimal part only
/// ([eE] )? optional exponent
/// [+-]? with an optional sign
/// \d+ followed by some digits
const NUMERIC_LITERAL_PATTERN: &str = r#"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?"#;
/// (\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?
/// ( | ) EITHER
/// \d+ integer part
/// (\.\d*)? with an optional decimal
/// ( | ) OR
/// \.\d+ decimal part only
/// ([eE] )? optional exponent
/// [+-]? with an optional sign
/// \d+ followed by some digits
const NUMERIC_LITERAL_PATTERN: &str = r#"(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?"#;

/// Single-quoted string. Note that like Rust strings, this can span multiple
/// lines.
Expand Down
10 changes: 9 additions & 1 deletion quadratic-core/src/formulas/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,21 @@ pub struct Parser<'a> {
impl<'a> Parser<'a> {
/// Constructs a parser for a file.
pub fn new(source_str: &'a str, tokens: &'a [Spanned<Token>], loc: Pos) -> Self {
Self {
let mut ret = Self {
source_str,
tokens,
cursor: None,

loc,
};

// Skip leading `=`
ret.next();
if ret.token_str() != "=" {
// Oops, no leading `=` so go back
ret.cursor = None;
}
ret
}

/// Returns the token at the cursor.
Expand Down
33 changes: 33 additions & 0 deletions quadratic-core/src/formulas/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@ impl GridProxy for PanicGridMock {
}
}

#[test]
fn test_formula_indirect() {
let form = parse_formula("CELL(3, 5)", Pos::new(1, 2)).unwrap();

make_stateless_grid_mock!(|pos| Some((pos.x * 10 + pos.y).to_string()));

assert_eq!(
FormulaErrorMsg::CircularReference,
form.eval_blocking(&mut GridMock, Pos::new(3, 5))
.unwrap_err()
.msg,
);

assert_eq!(
(3 * 10 + 5).to_string(),
eval_to_string(&mut GridMock, "CELL(3, 5)"),
);
}

#[test]
fn test_formula_cell_ref() {
let form = parse_formula("SUM($C$4, $A0, D$n6, A0, ZB2)", Pos::new(3, 4)).unwrap();
Expand Down Expand Up @@ -181,6 +200,20 @@ fn test_formula_array_op() {
);
}

#[test]
fn test_leading_equals() {
assert_eq!("7", eval_to_string(&mut PanicGridMock, "=3+4"));
assert_eq!("7", eval_to_string(&mut PanicGridMock, "= 3+4"));
}

/// Regression test for quadratic#253
#[test]
fn test_hyphen_after_cell_ref() {
make_stateless_grid_mock!(|_| Some("30".to_string()));
assert_eq!("25", eval_to_string(&mut GridMock, "Z1 - 5"));
assert_eq!("25", eval_to_string(&mut GridMock, "Z1-5"));
}

fn eval_to_string(grid: &mut impl GridProxy, s: &str) -> String {
eval(grid, s).unwrap().to_string()
}
Expand Down
4 changes: 3 additions & 1 deletion quadratic-core/src/formulas/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ impl Spanned<Value> {
.with_span(self.span)),
}
}

pub fn to_integer(&self) -> FormulaResult<i64> {
Ok(self.to_number()?.round() as i64)
}
pub fn to_bool(&self) -> FormulaResult<bool> {
match &self.inner {
Value::Bool(b) => Ok(*b),
Expand Down