Skip to content

Commit

Permalink
Add HLOOKUP, VLOOKUP, and XLOOKUP (#569)
Browse files Browse the repository at this point in the history
  • Loading branch information
HactarCE committed Jun 27, 2023
1 parent be0da02 commit 0e1ee87
Show file tree
Hide file tree
Showing 19 changed files with 1,585 additions and 244 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.12"
version = "0.1.13"
authors = ["Andrew Farkas <andrew.farkas@quadratic.to>"]
edition = "2021"
description = "Infinite data grid with Python, JavaScript, and SQL built-in"
Expand Down
32 changes: 27 additions & 5 deletions quadratic-core/src/bin/docgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,36 @@ fn main() {
output.push_str(&format!("## {}\n\n", category.name));
output.push_str(category.docs);

// Table header.
// Table header
output.push_str("| **Function** | **Description** |\n");
output.push_str("| ------------ | --------------- |\n");
for func in (category.get_functions)() {
let usages = format!("`{}`", func.usages_string());
let doc = func.doc.replace('\n', " ");
output.push_str(&format!("| {usages} | {doc} |\n"));

let all_functions = (category.get_functions)();
let mut functions_that_need_their_own_sections = vec![];
for func in &all_functions {
let docs = func.docs_string();
// Check for multiple paragraphs (one blank line)
if docs.contains("\n\n") {
functions_that_need_their_own_sections.push(func)
} else {
let usages = format!("`{}`", func.usages_string());
let docs = docs.replace('\n', " ");
output.push_str(&format!("| {usages} | {docs} |\n"));
}
}
for func in functions_that_need_their_own_sections {
output.push('\n');
output.push_str(&format!("### {}\n\n", func.name));
output.push_str(&format!("`{}`\n\n", func.usages_string()));
output.push_str("Examples:\n\n");
for example in func.examples {
output.push_str(&format!("- `{example}`\n"));
}
output.push('\n');
output.push_str(&func.docs_string().replace("\n#", "\n####"));
output.push('\n');
}

output.push('\n');
}

Expand Down
83 changes: 83 additions & 0 deletions quadratic-core/src/formulas/array_size.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use std::fmt;

use super::FormulaErrorMsg;

/// Size of a region or array.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct ArraySize {
/// Width (number of columns)
pub w: u32,
/// Height (number of rows)
pub h: u32,
}
impl fmt::Display for ArraySize {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let ArraySize { w, h } = self;
write!(f, "{w}x{h}")
}
}
impl std::ops::Index<Axis> for ArraySize {
type Output = u32;

fn index(&self, index: Axis) -> &Self::Output {
match index {
Axis::X => &self.w,
Axis::Y => &self.h,
}
}
}
impl std::ops::IndexMut<Axis> for ArraySize {
fn index_mut(&mut self, index: Axis) -> &mut Self::Output {
match index {
Axis::X => &mut self.w,
Axis::Y => &mut self.h,
}
}
}
impl ArraySize {
pub fn flatten_index(self, x: u32, y: u32) -> Result<usize, FormulaErrorMsg> {
let x = if self.w > 1 { x } else { 0 };
let y = if self.h > 1 { y } else { 0 };
if x < self.w && y < self.h {
Ok((x + y * self.w) as usize)
} else {
Err(FormulaErrorMsg::IndexOutOfBounds)
}
}
}

/// Horizontal or vertical axis.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Axis {
/// Horizontal axis / columns
X = 0,
/// Vertical axis / rows
Y = 1,
}
impl Axis {
pub const ALL: [Axis; 2] = [Axis::X, Axis::Y];

pub fn other_axis(self) -> Self {
match self {
Axis::X => Axis::Y,
Axis::Y => Axis::X,
}
}

pub fn width_height_str(self) -> &'static str {
match self {
Axis::X => "width",
Axis::Y => "height",
}
}
pub fn rows_cols_str(self, len: u32) -> String {
format!(
"{len} {}{}",
match self {
Axis::X => "column",
Axis::Y => "row",
},
if len == 1 { "" } else { "s" },
)
}
}
9 changes: 5 additions & 4 deletions quadratic-core/src/formulas/ast.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use futures::future::{FutureExt, LocalBoxFuture};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use smallvec::smallvec;
use std::fmt;

use super::*;
Expand Down Expand Up @@ -149,15 +150,15 @@ impl AstNode {
return Err(FormulaErrorMsg::ArrayTooBig.with_span(self.span));
}

let mut flat_array = vec![];
let mut flat_array = smallvec![];
for y in y1..=y2 {
for x in x1..=x2 {
let cell_ref = CellRef::absolute(Pos { x, y });
flat_array.push(ctx.get_cell(cell_ref, self.span).await?.inner);
}
}

Array::from_row_major_iter(width, height, flat_array)?.into()
Array::new_row_major(width, height, flat_array)?.into()
}

// Other operator/function
Expand Down Expand Up @@ -188,7 +189,7 @@ impl AstNode {
let width = a[0].len();
let height = a.len();

let mut flat_array = vec![];
let mut flat_array = smallvec![];
for row in a {
if row.len() != width {
return Err(FormulaErrorMsg::NonRectangularArray.with_span(self.span));
Expand All @@ -198,7 +199,7 @@ impl AstNode {
}
}

Array::from_row_major_iter(width as u32, height as u32, flat_array)?.into()
Array::new_row_major(width as u32, height as u32, flat_array)?.into()
}

// Single cell references return 1x1 arrays for Excel compatibility.
Expand Down
40 changes: 5 additions & 35 deletions quadratic-core/src/formulas/criteria.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
//! Mimic Excel's criteria in functions such as `SUMIF()`.
//!
//! This entire file feels really janky and awful but this is my best the
//! behavior Excel has.
//! This entire file feels really janky and awful but this is my best attempt at
//! mimicking the behavior Excel has.

use itertools::Itertools;
use regex::{Regex, RegexBuilder};
use regex::Regex;

use super::{
Array, BasicValue, CoerceInto, FormulaError, FormulaErrorMsg, FormulaResult, SpannableIterExt,
Spanned,
wildcard_pattern_to_regex, Array, BasicValue, CoerceInto, FormulaError, FormulaErrorMsg,
FormulaResult, SpannableIterExt, Spanned,
};

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -137,36 +137,6 @@ impl Criterion {
}
}

fn wildcard_pattern_to_regex(s: &str) -> Result<Regex, FormulaError> {
let mut chars = s.chars();
let mut regex_string = String::new();
regex_string.push('^'); // Match whole string using `^...$`.
while let Some(c) = chars.next() {
match c {
// Escape the next character, if there is one. Otherwise ignore.
'~' => {
if let Some(c) = chars.next() {
regex_string.push_str(&regex::escape(&c.to_string()))
}
}

'?' => regex_string.push('.'),
'*' => regex_string.push_str(".*"),
_ => regex_string.push_str(&regex::escape(&c.to_string())),
}
}
regex_string.push('$'); // Match whole string using `^...$`.
RegexBuilder::new(&regex_string)
.case_insensitive(true)
.build()
.map_err(|e| {
FormulaErrorMsg::InternalError(
format!("error building regex for criterion {s:?}: {e}").into(),
)
.without_span()
})
}

fn strip_compare_fn_prefix(s: &str) -> Option<(CompareFn, &str)> {
None.or_else(|| s.strip_prefix("==").map(|rest| (CompareFn::Eql, rest)))
.or_else(|| s.strip_prefix("=").map(|rest| (CompareFn::Eql, rest)))
Expand Down
10 changes: 5 additions & 5 deletions quadratic-core/src/formulas/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@ use super::*;

macro_rules! zip_map_impl {
($arrays:ident.zip_map(|$args_buffer:ident| $eval_f:expr)) => {{
let (width, height) = Value::common_array_size($arrays)?;
let ArraySize { w, h } = Value::common_array_size($arrays)?;

let mut $args_buffer = Vec::with_capacity($arrays.into_iter().len());

// If the result is a single value, return that value instead of a 1x1
// array. This isn't just an optimization; it's important for Excel
// compatibility.
if width == 1 && height == 1 {
if w == 1 && h == 1 {
for array in $arrays {
$args_buffer.push(array.basic_value()?);
}
return Ok(Value::Single($eval_f));
}

let mut values = Vec::with_capacity(width as usize * height as usize);
for (x, y) in Array::indices(width, height) {
let mut values = smallvec::SmallVec::with_capacity(w as usize * h as usize);
for (x, y) in Array::indices(w, h) {
$args_buffer.clear();
for array in $arrays {
$args_buffer.push(array.get(x, y)?);
Expand All @@ -28,7 +28,7 @@ macro_rules! zip_map_impl {
values.push($eval_f);
}

let result = Array::from_row_major_iter(width, height, values)?;
let result = Array::new_row_major(w, h, values)?;
Ok(Value::Array(result))
}};
}
Expand Down
Loading

1 comment on commit 0e1ee87

@vercel
Copy link

@vercel vercel bot commented on 0e1ee87 Jun 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

quadratic – ./

quadratic-git-main-quadratic.vercel.app
quadratic-quadratic.vercel.app
quadratic-nu.vercel.app

Please sign in to comment.