Skip to content

Commit 6288746

Browse files
committed
[IMP] formulas: accept ranges as meta arguments
Why this commit ? We would like to introduce the SUBTOTAL formula: This formula has the particularity of taking meta arguments as input, and based on certain criteria, returning the value related to the passed reference. Compared to other formulas using meta arguments, using the values belonging to the cells of the meta arguments is a new behavior. So, for this function to be viable, two things must be done: - compute the values of the passed meta references (which was not done until now) - compute all the values of the cells belonging to the matrix when a range is passed as a meta reference This last point (computing all the cell values when a meta range is passed) implies differentiating between simple meta arguments and matrix meta arguments in the code. Hence the main purpose of this commit. Two other implications of this commit: - This commit enables vectorization for meta arguments. - We revised the way arguments were defined in meta functions, resulting in a complete redefinition for certain functions, such as COLUMNS and ROWS, which no longer use meta arguments. This change involved redefining the error type for references whose sheetname doesn't exist. This also promotes consistency. Task: 4873384 Part-of: #6774 Signed-off-by: Lucas Lefèvre (lul) <[email protected]>
1 parent 994f1cb commit 6288746

File tree

15 files changed

+507
-118
lines changed

15 files changed

+507
-118
lines changed

src/formulas/compiler.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ function compileTokensOrThrow(tokens: Token[]): CompiledFormula {
138138
const argTypes = argDefinition.type || [];
139139

140140
// detect when an argument need to be evaluated as a meta argument
141-
const isMeta = argTypes.includes("META");
141+
const isMeta = argTypes.includes("META") || argTypes.includes("RANGE<META>");
142142
const hasRange = argTypes.some((t) => isRangeType(t));
143143

144144
compiledArgs.push(compileAST(currentArg, isMeta, hasRange));
@@ -178,11 +178,11 @@ function compileTokensOrThrow(tokens: Token[]): CompiledFormula {
178178
case "STRING":
179179
return code.return(`this.literalValues.strings[${stringCount++}]`);
180180
case "REFERENCE":
181-
if ((!isMeta && ast.value.includes(":")) || hasRange) {
182-
return code.return(`range(deps[${dependencyCount++}])`);
183-
} else {
184-
return code.return(`ref(deps[${dependencyCount++}], ${isMeta ? "true" : "false"})`);
185-
}
181+
return code.return(
182+
`${ast.value.includes(":") || hasRange ? `range` : `ref`}(deps[${dependencyCount++}], ${
183+
isMeta ? "true" : "false"
184+
})`
185+
);
186186
case "FUNCALL":
187187
const args = compileFunctionArgs(ast).map((arg) => arg.assignResultToVariable());
188188
code.append(...args);

src/functions/arguments.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const ARG_TYPES: ArgType[] = [
1717
"RANGE<NUMBER>",
1818
"RANGE<STRING>",
1919
"META",
20+
"RANGE<META>",
2021
];
2122

2223
export function arg(definition: string, description: string = ""): ArgDefinition {
@@ -266,6 +267,8 @@ export function _argTargeting(
266267
// Argument validation
267268
//------------------------------------------------------------------------------
268269

270+
const META_TYPES: ArgType[] = ["META", "RANGE<META>"];
271+
269272
export function validateArguments(descr: FunctionDescription) {
270273
if (descr.nbrArgRepeating && descr.nbrArgOptional >= descr.nbrArgRepeating) {
271274
throw new Error(`Function ${descr.name} has more optional arguments than repeatable ones.`);
@@ -274,9 +277,12 @@ export function validateArguments(descr: FunctionDescription) {
274277
let foundRepeating = false;
275278
let consecutiveRepeating = false;
276279
for (const current of descr.args) {
277-
if (current.type.includes("META") && current.type.length > 1) {
280+
if (
281+
current.type.some((t) => META_TYPES.includes(t)) &&
282+
current.type.some((t) => !META_TYPES.includes(t))
283+
) {
278284
throw new Error(
279-
`Function ${descr.name} has an argument that has been declared with more than one type whose type 'META'. The 'META' type can only be declared alone.`
285+
`Function ${descr.name} has a mix of META and non-META types in the same argument: ${current.type}.`
280286
);
281287
}
282288

src/functions/module_info.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { getFullReference, splitReference } from "../helpers";
22
import { setXcToFixedReferenceType } from "../helpers/reference_type";
33
import { _t } from "../translation";
4-
import { AddFunctionDescription, CellValueType, FunctionResultObject, Maybe } from "../types";
4+
import {
5+
AddFunctionDescription,
6+
CellValueType,
7+
FunctionResultObject,
8+
Matrix,
9+
Maybe,
10+
} from "../types";
511
import { CellErrorType, EvaluationError } from "../types/errors";
612
import { arg } from "./arguments";
713
import { isEvaluationError, toString } from "./helpers";
@@ -18,9 +24,9 @@ export const CELL = {
1824
"info_type (string)",
1925
_t("The type of information requested. Can be one of %s", CELL_INFO_TYPES.join(", "))
2026
),
21-
arg("reference (meta)", _t("The reference to the cell.")),
27+
arg("reference (meta, range<meta>)", _t("The reference to the cell.")),
2228
],
23-
compute: function (info: Maybe<FunctionResultObject>, reference: Maybe<{ value: string }>) {
29+
compute: function (info: Maybe<FunctionResultObject>, reference: Matrix<{ value: string }>) {
2430
const _info = toString(info).toLowerCase();
2531
if (!CELL_INFO_TYPES.includes(_info)) {
2632
return new EvaluationError(
@@ -29,9 +35,8 @@ export const CELL = {
2935
}
3036

3137
const sheetId = this.__originSheetId;
32-
const _reference = toString(reference);
33-
const topLeftReference = _reference.includes(":") ? _reference.split(":")[0] : _reference;
34-
let { sheetName, xc } = splitReference(topLeftReference);
38+
const _reference = reference[0][0].value;
39+
let { sheetName, xc } = splitReference(_reference);
3540
// only put the sheet name if the referenced range is in another sheet than the cell the formula is on
3641
sheetName = sheetName === this.getters.getSheetName(sheetId) ? undefined : sheetName;
3742
const fixedRef = getFullReference(sheetName, setXcToFixedReferenceType(xc, "colrow"));

src/functions/module_lookup.ts

Lines changed: 58 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -118,16 +118,13 @@ export const COLUMN = {
118118
description: _t("Column number of a specified cell."),
119119
args: [
120120
arg(
121-
"cell_reference (meta, default='this cell')",
121+
"cell_reference (meta, range<meta>, default='this cell')",
122122
_t(
123123
"The cell whose column number will be returned. Column A corresponds to 1. By default, the function use the cell in which the formula is entered."
124124
)
125125
),
126126
],
127-
compute: function (cellReference: Maybe<{ value: string }>) {
128-
if (isEvaluationError(cellReference?.value)) {
129-
return cellReference;
130-
}
127+
compute: function (cellReference: Matrix<{ value: string }> | undefined) {
131128
if (cellReference === undefined) {
132129
if (this.__originCellPosition?.col === undefined) {
133130
return new EvaluationError(
@@ -136,21 +133,22 @@ export const COLUMN = {
136133
)
137134
);
138135
}
139-
return this.__originCellPosition!.col! + 1;
136+
return this.__originCellPosition.col + 1;
140137
}
141-
142-
const zone = this.getters.getRangeFromSheetXC(
138+
if (cellReference[0][0] === undefined) {
139+
return new EvaluationError(_t("The range is out of bounds."));
140+
}
141+
if (cellReference[0][0].value === CellErrorType.InvalidReference) {
142+
return cellReference[0][0]; // return the same error
143+
}
144+
const left = this.getters.getRangeFromSheetXC(
143145
this.getters.getActiveSheetId(),
144-
cellReference.value
145-
).zone;
146-
147-
if (zone.left === zone.right) {
148-
return zone.left + 1;
146+
cellReference[0][0].value
147+
).zone.left;
148+
if (cellReference.length === 1) {
149+
return left + 1;
149150
}
150-
151-
return generateMatrix(zone.right - zone.left + 1, 1, (col, row) => ({
152-
value: zone.left + col + 1,
153-
}));
151+
return generateMatrix(cellReference.length, 1, (col, row) => ({ value: left + col + 1 }));
154152
},
155153
isExported: true,
156154
} satisfies AddFunctionDescription;
@@ -161,13 +159,16 @@ export const COLUMN = {
161159

162160
export const COLUMNS = {
163161
description: _t("Number of columns in a specified array or range."),
164-
args: [arg("range (meta)", _t("The range whose column count will be returned."))],
165-
compute: function (range: { value: string }) {
166-
if (isEvaluationError(range?.value)) {
167-
return range;
162+
args: [arg("range (any, range<any>)", _t("The range whose column count will be returned."))],
163+
compute: function (range: Arg) {
164+
const _range = toMatrix(range);
165+
if (_range[0][0] === undefined) {
166+
return new EvaluationError(_t("The range is out of bounds."));
167+
}
168+
if (_range[0][0].value === CellErrorType.InvalidReference) {
169+
return _range[0][0];
168170
}
169-
const zone = toZone(range.value);
170-
return zone.right - zone.left + 1;
171+
return _range.length;
171172
},
172173
isExported: true,
173174
} satisfies AddFunctionDescription;
@@ -504,16 +505,13 @@ export const ROW = {
504505
description: _t("Row number of a specified cell."),
505506
args: [
506507
arg(
507-
"cell_reference (meta, default='this cell')",
508+
"cell_reference (meta, range<meta>, default='this cell')",
508509
_t(
509510
"The cell whose row number will be returned. By default, this function uses the cell in which the formula is entered."
510511
)
511512
),
512513
],
513-
compute: function (cellReference: Maybe<{ value: string }>) {
514-
if (isEvaluationError(cellReference?.value)) {
515-
return cellReference;
516-
}
514+
compute: function (cellReference: Matrix<{ value: string }> | undefined) {
517515
if (cellReference === undefined) {
518516
if (this.__originCellPosition?.row === undefined) {
519517
return new EvaluationError(
@@ -522,21 +520,22 @@ export const ROW = {
522520
)
523521
);
524522
}
525-
return this.__originCellPosition!.row! + 1;
523+
return this.__originCellPosition.row + 1;
526524
}
527-
528-
const zone = this.getters.getRangeFromSheetXC(
525+
if (cellReference[0][0] === undefined) {
526+
return new EvaluationError(_t("The range is out of bounds."));
527+
}
528+
if (cellReference[0][0].value === CellErrorType.InvalidReference) {
529+
return cellReference[0][0]; // return the same error
530+
}
531+
const top = this.getters.getRangeFromSheetXC(
529532
this.getters.getActiveSheetId(),
530-
cellReference.value
531-
).zone;
532-
533-
if (zone.top === zone.bottom) {
534-
return zone.top + 1;
533+
cellReference[0][0].value
534+
).zone.top;
535+
if (cellReference[0].length === 1) {
536+
return top + 1;
535537
}
536-
537-
return generateMatrix(1, zone.bottom - zone.top + 1, (col, row) => ({
538-
value: zone.top + row + 1,
539-
}));
538+
return generateMatrix(1, cellReference[0].length, (col, row) => ({ value: top + row + 1 }));
540539
},
541540
isExported: true,
542541
} satisfies AddFunctionDescription;
@@ -547,13 +546,16 @@ export const ROW = {
547546

548547
export const ROWS = {
549548
description: _t("Number of rows in a specified array or range."),
550-
args: [arg("range (meta)", _t("The range whose row count will be returned."))],
551-
compute: function (range: { value: string }) {
552-
if (isEvaluationError(range?.value)) {
553-
return range;
549+
args: [arg("range (any, range<any>)", _t("The range whose row count will be returned."))],
550+
compute: function (range: Arg) {
551+
const _range = toMatrix(range);
552+
if (_range[0][0] === undefined) {
553+
return new EvaluationError(_t("The range is out of bounds."));
554+
}
555+
if (_range[0][0].value === CellErrorType.InvalidReference) {
556+
return _range[0][0];
554557
}
555-
const zone = toZone(range.value);
556-
return zone.bottom - zone.top + 1;
558+
return _range[0].length;
557559
},
558560
isExported: true,
559561
} satisfies AddFunctionDescription;
@@ -975,7 +977,7 @@ export const OFFSET = {
975977
),
976978
args: [
977979
arg(
978-
"cell_reference (meta)",
980+
"cell_reference (meta, range<meta>)",
979981
_t("The starting point from which to count the offset rows and columns.")
980982
),
981983
arg("offset_rows (number)", _t("The number of rows to offset by.")),
@@ -990,26 +992,26 @@ export const OFFSET = {
990992
),
991993
],
992994
compute: function (
993-
cellReference: Maybe<{ value: string }>,
995+
cellReference: Matrix<{ value: string }>,
994996
offsetRows: Maybe<FunctionResultObject>,
995997
offsetColumns: Maybe<FunctionResultObject>,
996998
height: Maybe<FunctionResultObject>,
997999
width: Maybe<FunctionResultObject>
9981000
) {
999-
if (isEvaluationError(cellReference?.value)) {
1000-
return cellReference;
1001+
if (isEvaluationError(cellReference[0][0].value)) {
1002+
return cellReference[0][0];
10011003
}
10021004

1003-
const _cellReference = cellReference?.value;
1004-
if (!_cellReference) {
1005+
const ref0 = cellReference[0][0].value;
1006+
if (!ref0) {
10051007
return new EvaluationError(
10061008
"In this context, the function OFFSET needs to have a cell or range in parameter."
10071009
);
10081010
}
1009-
const zone = toZone(_cellReference);
1011+
const zone = toZone(ref0);
10101012

1011-
let offsetHeight = zone.bottom - zone.top + 1;
1012-
let offsetWidth = zone.right - zone.left + 1;
1013+
let offsetHeight = cellReference[0].length;
1014+
let offsetWidth = cellReference.length;
10131015

10141016
if (height) {
10151017
const _height = toNumber(height, this.locale);
@@ -1031,7 +1033,7 @@ export const OFFSET = {
10311033
offsetWidth = _width;
10321034
}
10331035

1034-
const { sheetName } = splitReference(_cellReference);
1036+
const { sheetName } = splitReference(ref0);
10351037

10361038
const sheetId =
10371039
(sheetName && this.getters.getSheetIdByName(sheetName)) || this.getters.getActiveSheetId();

src/plugins/ui_core_views/cell_evaluation/compilation_parameters.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { functionRegistry } from "../../../functions";
2-
import { getFullReference, intersection, isZoneValid, zoneToXc } from "../../../helpers";
2+
import { getFullReference, intersection, isZoneValid, toXC, zoneToXc } from "../../../helpers";
33
import { ModelConfig } from "../../../model";
44
import { _t } from "../../../translation";
55
import {
@@ -74,13 +74,14 @@ class CompilationParametersBuilder {
7474
if (rangeError) {
7575
return rangeError;
7676
}
77+
// the compiler guarantees only single cell ranges reach this part of the code
78+
const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };
7779
if (isMeta) {
80+
this.computeCell(position); // ensure the cell is computed: sometimes formulas that use meta parameters ending by return the value of the corresponding reference
7881
// Use zoneToXc of zone instead of getRangeString to avoid sending unbounded ranges
7982
const sheetName = this.getters.getSheetName(range.sheetId);
8083
return { value: getFullReference(sheetName, zoneToXc(range.zone)) };
8184
}
82-
// the compiler guarantees only single cell ranges reach this part of the code
83-
const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };
8485
return this.computeCell(position);
8586
}
8687

@@ -92,7 +93,7 @@ class CompilationParametersBuilder {
9293
* Note that each col is possibly sparse: it only contain the values of cells
9394
* that are actually present in the grid.
9495
*/
95-
private range(range: Range): Matrix<FunctionResultObject> {
96+
private range(range: Range, isMeta: boolean): Matrix<FunctionResultObject> {
9697
const rangeError = this.getRangeError(range);
9798
if (rangeError) {
9899
return [[rangeError]];
@@ -107,22 +108,29 @@ class CompilationParametersBuilder {
107108
if (!_zone) {
108109
return [[]];
109110
}
111+
112+
// Performance issue: cache the range results to avoid recomputing them
110113
const { top, left, bottom, right } = zone;
111-
const cacheKey = `${sheetId}-${top}-${left}-${bottom}-${right}`;
114+
const cacheKey = `${sheetId}-${top}-${left}-${bottom}-${right}-${isMeta}`;
112115
if (cacheKey in this.rangeCache) {
113116
return this.rangeCache[cacheKey];
114117
}
115118

116119
const height = _zone.bottom - _zone.top + 1;
117120
const width = _zone.right - _zone.left + 1;
118121
const matrix: Matrix<FunctionResultObject> = new Array(width);
122+
const sheetName = this.getters.getSheetName(range.sheetId);
123+
119124
// Performance issue: nested loop is faster than a map here
120125
for (let col = _zone.left; col <= _zone.right; col++) {
121126
const colIndex = col - _zone.left;
122127
matrix[colIndex] = new Array(height);
123128
for (let row = _zone.top; row <= _zone.bottom; row++) {
124129
const rowIndex = row - _zone.top;
125-
matrix[colIndex][rowIndex] = this.computeCell({ sheetId, col, row });
130+
const computedCell = this.computeCell({ sheetId, col, row });
131+
matrix[colIndex][rowIndex] = isMeta
132+
? { value: getFullReference(sheetName, toXC(col, row)) }
133+
: computedCell;
126134
}
127135
}
128136

@@ -135,7 +143,7 @@ class CompilationParametersBuilder {
135143
return new InvalidReferenceError();
136144
}
137145
if (range.invalidSheetName) {
138-
return new EvaluationError(_t("Invalid sheet name: %s", range.invalidSheetName));
146+
return new InvalidReferenceError(_t("Invalid sheet name: %s", range.invalidSheetName));
139147
}
140148
return undefined;
141149
}

src/types/functions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export type ArgType =
1616
| "RANGE<DATE>"
1717
| "RANGE<STRING>"
1818
| "RANGE<ANY>"
19-
| "META";
19+
| "META"
20+
| "RANGE<META>";
2021

2122
export interface ArgDefinition {
2223
acceptMatrix?: boolean;

src/types/misc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export type ReferenceDenormalizer = (
173173
paramNumber: number
174174
) => FunctionResultObject;
175175

176-
export type EnsureRange = (range: Range) => Matrix<FunctionResultObject>;
176+
export type EnsureRange = (range: Range, isMeta: boolean) => Matrix<FunctionResultObject>;
177177

178178
export type GetSymbolValue = (symbolName: string) => Arg;
179179

0 commit comments

Comments
 (0)