Skip to content
Open
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
5 changes: 3 additions & 2 deletions src/sheet-engine/core/modules/sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,13 +417,14 @@ export function spillSortResult(
const formulaString = formulaResult?.f;
const formulaValue = formulaResult?.v;

// make sure it is a SORT formula result
// make sure it is a formula result from a function that spills an array
if (
typeof formulaString !== 'string' ||
!(
/= *SORT\s*\(/i.test(formulaString) ||
/= *XLOOKUP\s*\(/i.test(formulaString) ||
/= *SEQUENCE\s*\(/i.test(formulaString)
/= *SEQUENCE\s*\(/i.test(formulaString) ||
/= *FILTER\s*\(/i.test(formulaString)
)
)
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { isArray2D } from "./../helper/array";
import { ERROR_NOT_AVAILABLE, ERROR_VALUE } from "./../error";

/**
* FILTER(range, condition1, [condition2, ...])
*
* Returns the subset of `range` whose rows (or columns) satisfy every condition.
* Each condition must be a single column (rowCount x 1) to filter rows, or a
* single row (1 x colCount) to filter columns — matching Excel/Sheets semantics.
* Conditions are combined with logical AND.
*/
function FILTER(range, ...conditions) {
if (!isArray2D(range) || conditions.length === 0) {
throw new Error(ERROR_VALUE);
}

const rowCount = range.length;
const colCount = range[0].length;

const isColumnCondition = (cond) =>
isArray2D(cond) && cond.length === rowCount && cond[0].length === 1;
const isRowCondition = (cond) =>
isArray2D(cond) && cond.length === 1 && cond[0].length === colCount;

if (conditions.every(isColumnCondition)) {
const filteredRows = range.filter((_row, r) =>
conditions.every((cond) => Boolean(cond[r][0])),
);

if (!filteredRows.length) {
throw new Error(ERROR_NOT_AVAILABLE);
}

return filteredRows;
}

if (conditions.every(isRowCondition)) {
const keptCols = [];
for (let c = 0; c < colCount; c += 1) {
if (conditions.every((cond) => Boolean(cond[0][c]))) {
keptCols.push(c);
}
}

if (!keptCols.length) {
throw new Error(ERROR_NOT_AVAILABLE);
}

return range.map((row) => keptCols.map((c) => row[c]));
}

throw new Error(ERROR_VALUE);
}

const CUSTOM_FORMULAS = {
FILTER,
};

export default CUSTOM_FORMULAS;
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { broadcastCompare } from "./../../helper/array";

export const SYMBOL = "=";

export default function func(exp1, exp2) {
return exp1 === exp2;
return broadcastCompare(exp1, exp2, (a, b) => a === b);
}

func.SYMBOL = SYMBOL;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as formulajs from "@fileverse-dev/formulajs";
import SUPPORTED_FORMULAS from "./../../supported-formulas";
import { ERROR_NAME } from "./../../error";
import en from "./../../../core/locale/en";
import CUSTOM_FORMULAS from "./../custom-formulas";

export const SYMBOL = SUPPORTED_FORMULAS;

Expand All @@ -21,7 +22,10 @@ export default function func(symbol) {
let result;

if (symbolParts.length === 1) {
if (formulajs[symbolParts[0]]) {
if (CUSTOM_FORMULAS[symbolParts[0]]) {
foundFormula = true;
result = CUSTOM_FORMULAS[symbolParts[0]](...params);
} else if (formulajs[symbolParts[0]]) {
foundFormula = true;
const functionDetails = FUNCTIONLIST_MAP_EN[symbolParts[0]] || null;
result = formulajs[symbolParts[0]](...params);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { broadcastCompare } from "./../../helper/array";

export const SYMBOL = ">=";

export default function func(exp1, exp2) {
return exp1 >= exp2;
return broadcastCompare(exp1, exp2, (a, b) => a >= b);
}

func.SYMBOL = SYMBOL;
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { broadcastCompare } from "./../../helper/array";

export const SYMBOL = ">";

export default function func(exp1, exp2) {
return exp1 > exp2;
return broadcastCompare(exp1, exp2, (a, b) => a > b);
}

func.SYMBOL = SYMBOL;
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { broadcastCompare } from "./../../helper/array";

export const SYMBOL = "<=";

export default function func(exp1, exp2) {
return exp1 <= exp2;
return broadcastCompare(exp1, exp2, (a, b) => a <= b);
}

func.SYMBOL = SYMBOL;
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { broadcastCompare } from "./../../helper/array";

export const SYMBOL = "<";

export default function func(exp1, exp2) {
return exp1 < exp2;
return broadcastCompare(exp1, exp2, (a, b) => a < b);
}

func.SYMBOL = SYMBOL;
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { broadcastCompare } from "./../../helper/array";

export const SYMBOL = "<>";

export default function func(exp1, exp2) {
return exp1 !== exp2;
return broadcastCompare(exp1, exp2, (a, b) => a !== b);
}

func.SYMBOL = SYMBOL;
39 changes: 39 additions & 0 deletions src/sheet-engine/formula-parser/helper/array.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* True when value is a 2D array (row of rows), the shape returned by `rangeValue`.
*
* @param {*} value
* @returns {Boolean}
*/
export function isArray2D(value) {
return Array.isArray(value) && Array.isArray(value[0]);
}

/**
* Apply a scalar comparator across operands, broadcasting element-wise when either
* side is a range (2D array) so relational operators work the same way they do in
* Excel/Sheets array formulas (eg. `$B:$B=D$1`).
*
* @param {*} exp1
* @param {*} exp2
* @param {Function} compare Scalar comparator, eg. `(a, b) => a === b`.
* @returns {Boolean|Array} Boolean for scalar/scalar, otherwise a 2D array matching
* the shape of whichever operand is a range.
*/
export function broadcastCompare(exp1, exp2, compare) {
const arr1 = isArray2D(exp1);
const arr2 = isArray2D(exp2);

if (!arr1 && !arr2) {
return compare(exp1, exp2);
}

if (arr1 && arr2) {
return exp1.map((row, r) =>
row.map((cell, c) => compare(cell, exp2[r]?.[c])),
);
}

return arr1
? exp1.map((row) => row.map((cell) => compare(cell, exp2)))
: exp2.map((row) => row.map((cell) => compare(exp1, cell)));
}
9 changes: 8 additions & 1 deletion src/sheet-engine/formula-parser/supported-formulas.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import * as formulajs from "@fileverse-dev/formulajs";

const SUPPORTED_FORMULAS = Object.keys(formulajs);
// Formulas implemented natively in this repo because the upstream formulajs
// fork doesn't (yet) provide them. See custom-formulas.js.
const CUSTOM_FORMULA_NAMES = ["FILTER"];

const SUPPORTED_FORMULAS = [
...Object.keys(formulajs),
...CUSTOM_FORMULA_NAMES,
];

export default SUPPORTED_FORMULAS;