Skip to content

Commit e3abf1d

Browse files
committed
cell position metadata in evaluation
1 parent f3439bb commit e3abf1d

File tree

17 files changed

+223
-177
lines changed

17 files changed

+223
-177
lines changed

packages/o-spreadsheet-engine/src/functions/create_compute_function.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function createComputeFunction(
3434
acceptToVectorize.push(!argDefinition.acceptMatrix);
3535
}
3636

37-
return applyVectorization(errorHandlingCompute.bind(this), args, acceptToVectorize);
37+
return applyVectorization(this, errorHandlingCompute, args, acceptToVectorize);
3838
}
3939

4040
function errorHandlingCompute(

packages/o-spreadsheet-engine/src/functions/helpers.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
NotAvailableError,
1212
errorTypes,
1313
} from "../types/errors";
14-
import { LookupCaches } from "../types/functions";
14+
import { EvalContext, LookupCaches } from "../types/functions";
1515
import { Locale } from "../types/locale";
1616
import {
1717
Arg,
@@ -499,6 +499,7 @@ type VectorArgType = "horizontal" | "vertical" | "matrix";
499499
* as ranges and invoke this helper directly within your `compute` implementation.
500500
*/
501501
export function applyVectorization(
502+
evalCtx: EvalContext,
502503
formula: (...args: Arg[]) => Matrix<FunctionResultObject> | FunctionResultObject,
503504
args: Arg[],
504505
acceptToVectorize: boolean[] | undefined = undefined
@@ -541,7 +542,7 @@ export function applyVectorization(
541542

542543
if (countVectorizedCol === 1 && countVectorizedRow === 1) {
543544
// either this function is not vectorized or it ends up with a 1x1 dimension
544-
return formula(...args);
545+
return formula.call(evalCtx, ...args);
545546
}
546547

547548
const getArgOffset: (i: number, j: number) => Arg[] = (i, j) =>
@@ -564,7 +565,16 @@ export function applyVectorization(
564565
_t("Array arguments to [[FUNCTION_NAME]] are of different size.")
565566
);
566567
}
567-
const singleCellComputeResult = formula(...getArgOffset(col, row));
568+
const basePosition = evalCtx.__originCellPosition;
569+
const ctx = { ...evalCtx };
570+
if (basePosition) {
571+
ctx.__originCellPosition = {
572+
col: basePosition.col + col,
573+
row: basePosition.row + row,
574+
sheetId: basePosition.sheetId,
575+
};
576+
}
577+
const singleCellComputeResult = formula.call(ctx, ...getArgOffset(col, row));
568578
// In the case where the user tries to vectorize arguments of an array formula, we will get an
569579
// array for every combination of the vectorized arguments, which will lead to a 3D matrix and
570580
// we won't be able to return the values.

packages/o-spreadsheet-engine/src/functions/module_logical.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export const IF = {
7272
],
7373
compute: function (logicalExpression: Arg, valueIfTrue: Arg, valueIfFalse: Arg) {
7474
if (isMultipleElementMatrix(logicalExpression)) {
75-
return applyVectorization(IF.compute, [logicalExpression, valueIfTrue, valueIfFalse]);
75+
return applyVectorization(this, IF.compute, [logicalExpression, valueIfTrue, valueIfFalse]);
7676
}
7777
const result = toBoolean(toScalar(logicalExpression)) ? valueIfTrue : valueIfFalse;
7878
return result ?? { value: 0 };
@@ -94,7 +94,7 @@ export const IFERROR = {
9494
],
9595
compute: function (value: Arg, valueIfError: Arg) {
9696
if (isMultipleElementMatrix(value)) {
97-
return applyVectorization(IFERROR.compute, [value, valueIfError]);
97+
return applyVectorization(this, IFERROR.compute, [value, valueIfError]);
9898
}
9999
const result = isEvaluationError(toScalar(value)?.value) ? valueIfError : value;
100100
return result ?? { value: 0 };
@@ -116,7 +116,7 @@ export const IFNA = {
116116
],
117117
compute: function (value: Arg, valueIfError: Arg) {
118118
if (isMultipleElementMatrix(value)) {
119-
return applyVectorization(IFNA.compute, [value, valueIfError]);
119+
return applyVectorization(this, IFNA.compute, [value, valueIfError]);
120120
}
121121
const result = toScalar(value)?.value === CellErrorType.NotAvailable ? valueIfError : value;
122122
return result ?? { value: 0 };
@@ -149,7 +149,7 @@ export const IFS = {
149149
}
150150
while (values.length > 0) {
151151
if (isMultipleElementMatrix(values[0])) {
152-
return applyVectorization(IFS.compute, values);
152+
return applyVectorization(this, IFS.compute, values);
153153
}
154154
const condition = toBoolean(toScalar(values.shift()));
155155
const valueIfTrue = values.shift();

packages/o-spreadsheet-engine/src/functions/module_lookup.ts

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import {
1010
import { toZone } from "../helpers/zones";
1111
import { _t } from "../translation";
1212
import { CellErrorType, EvaluationError, InvalidReferenceError } from "../types/errors";
13-
import { AddFunctionDescription } from "../types/functions";
14-
import { Arg, FunctionResultObject, Matrix, Maybe, Zone } from "../types/misc";
13+
import { AddFunctionDescription, EvalContext } from "../types/functions";
14+
import { Arg, FunctionResultObject, Matrix, Maybe, PivotCacheItem, Zone } from "../types/misc";
1515
import { arg } from "./arguments";
1616
import { expectNumberGreaterThanOrEqualToOne } from "./helper_assert";
1717
import {
@@ -786,6 +786,15 @@ export const XLOOKUP = {
786786
// Pivot functions
787787
//--------------------------------------------------------------------------
788788

789+
function addPivotMetaDataToContext(context: EvalContext, item: PivotCacheItem) {
790+
const { __originCellPosition: position, cellPositionMetaData: positionMap } = context;
791+
if (position) {
792+
const existingData = positionMap.get(position) || {};
793+
existingData["pivot"] = item;
794+
positionMap.set(position, existingData);
795+
}
796+
}
797+
789798
// PIVOT.VALUE
790799

791800
export const PIVOT_VALUE = {
@@ -804,8 +813,13 @@ export const PIVOT_VALUE = {
804813
const _pivotFormulaId = toString(formulaId);
805814
const _measure = toString(measureName);
806815
const pivotId = getPivotId(_pivotFormulaId, this.getters);
807-
assertMeasureExist(pivotId, _measure, this.getters);
808-
assertDomainLength(domainArgs);
816+
try {
817+
assertMeasureExist(pivotId, _measure, this.getters);
818+
assertDomainLength(domainArgs);
819+
} catch (e) {
820+
addPivotMetaDataToContext(this, { type: "error", pivotId });
821+
return e;
822+
}
809823
const pivot = this.getters.getPivot(pivotId);
810824
const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
811825

@@ -817,6 +831,7 @@ export const PIVOT_VALUE = {
817831
pivot.init({ reload: pivot.needsReevaluation });
818832
const error = pivot.assertIsValid({ throwOnError: false });
819833
if (error) {
834+
addPivotMetaDataToContext(this, { type: "error", pivotId });
820835
return error;
821836
}
822837

@@ -825,6 +840,7 @@ export const PIVOT_VALUE = {
825840
"Consider using a dynamic pivot formula: %s. Or re-insert the static pivot from the Data menu.",
826841
`=PIVOT(${_pivotFormulaId})`
827842
);
843+
addPivotMetaDataToContext(this, { type: "error", pivotId });
828844
return {
829845
value: CellErrorType.GenericError,
830846
message: _t("Dimensions don't match the pivot definition") + ". " + suggestion,
@@ -834,6 +850,12 @@ export const PIVOT_VALUE = {
834850
if (this.getters.getActiveSheetId() === this.__originSheetId) {
835851
this.getters.getPivotPresenceTracker(pivotId)?.trackValue(_measure, domain);
836852
}
853+
addPivotMetaDataToContext(this, {
854+
type: "static",
855+
pivotId,
856+
pivotCell: { type: "VALUE", measure: _measure, domain },
857+
});
858+
837859
return pivot.getPivotCellValueAndFormat(_measure, domain);
838860
},
839861
} satisfies AddFunctionDescription;
@@ -853,20 +875,27 @@ export const PIVOT_HEADER = {
853875
) {
854876
const _pivotFormulaId = toString(pivotId);
855877
const _pivotId = getPivotId(_pivotFormulaId, this.getters);
856-
assertDomainLength(domainArgs);
878+
try {
879+
assertDomainLength(domainArgs);
880+
} catch (e) {
881+
addPivotMetaDataToContext(this, { type: "error", pivotId: _pivotId });
882+
return e;
883+
}
857884
const pivot = this.getters.getPivot(_pivotId);
858885
const coreDefinition = this.getters.getPivotCoreDefinition(_pivotId);
859886
addPivotDependencies(this, coreDefinition, []);
860887
pivot.init({ reload: pivot.needsReevaluation });
861888
const error = pivot.assertIsValid({ throwOnError: false });
862889
if (error) {
890+
addPivotMetaDataToContext(this, { type: "error", pivotId: _pivotId });
863891
return error;
864892
}
865893
if (!pivot.areDomainArgsFieldsValid(domainArgs)) {
866894
const suggestion = _t(
867895
"Consider using a dynamic pivot formula: %s. Or re-insert the static pivot from the Data menu.",
868896
`=PIVOT(${_pivotFormulaId})`
869897
);
898+
addPivotMetaDataToContext(this, { type: "error", pivotId: _pivotId });
870899
return {
871900
value: CellErrorType.GenericError,
872901
message: _t("Dimensions don't match the pivot definition") + ". " + suggestion,
@@ -876,11 +905,32 @@ export const PIVOT_HEADER = {
876905
if (this.getters.getActiveSheetId() === this.__originSheetId) {
877906
this.getters.getPivotPresenceTracker(_pivotId)?.trackHeader(domain);
878907
}
908+
879909
const lastNode = domain.at(-1);
880910
if (lastNode?.field === "measure") {
881-
return pivot.getPivotMeasureValue(toString(lastNode.value), domain);
911+
const measure = toString(lastNode.value);
912+
addPivotMetaDataToContext(this, {
913+
type: "static",
914+
pivotId: _pivotId,
915+
pivotCell: { type: "MEASURE_HEADER", measure, domain: domain.slice(0, -1) },
916+
});
917+
918+
return pivot.getPivotMeasureValue(measure, domain);
882919
}
883920
const { value, format } = pivot.getPivotHeaderValueAndFormat(domain);
921+
922+
const columns = pivot.definition.columns;
923+
const isColumnHeader = columns.some((col) => col.nameWithGranularity === domain[0]?.field);
924+
addPivotMetaDataToContext(this, {
925+
type: "static",
926+
pivotId: _pivotId,
927+
pivotCell: {
928+
type: "HEADER",
929+
domain,
930+
dimension: isColumnHeader ? "COL" : "ROW",
931+
},
932+
});
933+
884934
return {
885935
value,
886936
format:
@@ -941,13 +991,16 @@ export const PIVOT = {
941991
pivot.init({ reload: pivot.needsReevaluation });
942992
const error = pivot.assertIsValid({ throwOnError: false });
943993
if (error) {
994+
addPivotMetaDataToContext(this, { type: "error", pivotId });
944995
return error;
945996
}
946997
const table = pivot.getCollapsedTableStructure();
947998
if (table.numberOfCells > PIVOT_MAX_NUMBER_OF_CELLS) {
999+
addPivotMetaDataToContext(this, { type: "error", pivotId });
9481000
return new EvaluationError(getPivotTooBigErrorMessage(table.numberOfCells, this.locale));
9491001
}
9501002
const cells = table.getPivotCells(pivotStyle);
1003+
addPivotMetaDataToContext(this, { type: "dynamic", pivotStyle, pivotId });
9511004

9521005
let headerRows = 0;
9531006
if (pivotStyle.displayColumnHeaders) {

packages/o-spreadsheet-engine/src/functions/module_math.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { splitReference } from "../helpers";
22
import { toZone } from "../helpers/zones";
3-
import { isSubtotalCell } from "../plugins/ui_feature/subtotal_evaluation";
43
import { _t } from "../translation";
54
import { EvaluatedCell } from "../types/cells";
65
import { DivisionByZeroError, EvaluationError } from "../types/errors";
76
import { AddFunctionDescription } from "../types/functions";
87
import {
98
Arg,
9+
CellPosition,
1010
FunctionResultNumber,
1111
FunctionResultObject,
1212
Matrix,
@@ -1386,6 +1386,19 @@ export const SUBTOTAL = {
13861386
code -= 100;
13871387
acceptHiddenCells = false;
13881388
}
1389+
1390+
const { __originCellPosition: position, cellPositionMetaData: positionMap } = this;
1391+
if (position) {
1392+
const existingData = positionMap.get(position) || {};
1393+
existingData["subtotal"] = true;
1394+
positionMap.set(position, existingData);
1395+
}
1396+
1397+
const isSubtotalCell = (cellPosition: CellPosition) => {
1398+
const cellMeta = positionMap.get(cellPosition);
1399+
return cellMeta?.subtotal === true;
1400+
};
1401+
13891402
if (code < 1 || code > 11) {
13901403
return new EvaluationError(
13911404
_t("The function code (%s) must be between 1 to 11 or 101 to 111.", code)
@@ -1410,7 +1423,7 @@ export const SUBTOTAL = {
14101423

14111424
for (let col = left; col <= right; col++) {
14121425
const cell = this.getters.getCell({ sheetId, col, row });
1413-
if (!cell || !isSubtotalCell(cell)) {
1426+
if (!cell || !isSubtotalCell({ sheetId, col, row })) {
14141427
evaluatedCellToKeep.push(this.getters.getEvaluatedCell({ sheetId, col, row }));
14151428
}
14161429
}

packages/o-spreadsheet-engine/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { isExportableToExcel } from "../../../formulas/helpers";
22
import { matrixMap } from "../../../functions/helpers";
3+
import { PositionMap } from "../../../helpers/cells/position_map";
34
import { toXC } from "../../../helpers/coordinates";
45
import { getItemId } from "../../../helpers/data_normalization";
56
import { cellPositions, positions } from "../../../helpers/zones";
@@ -159,6 +160,8 @@ export class EvaluationPlugin extends CoreViewPlugin {
159160
"getSpreadZone",
160161
"getArrayFormulaSpreadingOn",
161162
"isEmpty",
163+
"getEvaluatedCellMetaDataMap",
164+
"getEvaluatedCellMetaData",
162165
] as const;
163166

164167
private shouldRebuildDependenciesGraph = true;
@@ -424,4 +427,17 @@ export class EvaluationPlugin extends CoreViewPlugin {
424427
}
425428
return undefined;
426429
}
430+
431+
getEvaluatedCellMetaDataMap(): PositionMap<{ [metaDataKey: string]: any }> {
432+
return this.evaluator.getCellPositionMetaDataMap();
433+
}
434+
435+
getEvaluatedCellMetaData(position: CellPosition) {
436+
const cachedAtPosition = this.evaluator.getCellPositionMetaDataMap().get(position);
437+
if (cachedAtPosition) {
438+
return cachedAtPosition;
439+
}
440+
const mainPosition = this.getArrayFormulaSpreadingOn(position);
441+
return mainPosition ? this.evaluator.getCellPositionMetaDataMap().get(mainPosition) : undefined;
442+
}
427443
}

packages/o-spreadsheet-engine/src/plugins/ui_core_views/cell_evaluation/evaluator.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
GetSymbolValue,
3535
isMatrix,
3636
Matrix,
37+
PivotCacheItem,
3738
RangeCompiledFormula,
3839
UID,
3940
Zone,
@@ -56,6 +57,8 @@ export class Evaluator {
5657
private blockedArrayFormulas = new PositionSet({});
5758
private spreadingRelations = new SpreadingRelation();
5859

60+
private cellPositionMetaData = new PositionMap<{ [metaDataKey: string]: any }>();
61+
5962
constructor(private readonly context: ModelConfig["custom"], getters: Getters) {
6063
this.getters = getters;
6164
this.compilationParams = buildCompilationParameters(
@@ -139,6 +142,7 @@ export class Evaluator {
139142
forwardSearch: new Map(),
140143
reverseSearch: new Map(),
141144
};
145+
this.compilationParams.evalContext.cellPositionMetaData = this.cellPositionMetaData;
142146
}
143147

144148
private createEmptyPositionSet() {
@@ -218,6 +222,7 @@ export class Evaluator {
218222
evaluateAllCells() {
219223
const start = performance.now();
220224
this.evaluatedCells = new PositionMap();
225+
this.cellPositionMetaData = new PositionMap<PivotCacheItem>();
221226
const ranges: BoundedRange[] = [];
222227
for (const sheetId of this.getters.getSheetIds()) {
223228
const zone = this.getters.getSheetZone(sheetId);
@@ -265,6 +270,10 @@ export class Evaluator {
265270
}
266271
}
267272

273+
getCellPositionMetaDataMap(): PositionMap<{ [metaDataKey: string]: any }> {
274+
return this.cellPositionMetaData;
275+
}
276+
268277
/**
269278
* Return the position of formulas blocked by the given positions
270279
* as well as all their dependencies.
@@ -325,7 +334,9 @@ export class Evaluator {
325334
const { left, bottom, right, top } = range.zone;
326335
for (let col = left; col <= right; col++) {
327336
for (let row = top; row <= bottom; row++) {
328-
this.evaluatedCells.delete({ sheetId: range.sheetId, col, row });
337+
const position = { sheetId: range.sheetId, col, row };
338+
this.cellPositionMetaData.delete(position);
339+
this.evaluatedCells.delete(position);
329340
}
330341
}
331342
}
@@ -545,6 +556,7 @@ export class Evaluator {
545556
continue;
546557
}
547558
this.evaluatedCells.delete(resultPosition);
559+
this.cellPositionMetaData.delete(resultPosition);
548560
}
549561
}
550562
const sheetId = position.sheetId;

0 commit comments

Comments
 (0)