Skip to content

Commit 1851586

Browse files
committed
[FIX] Pivots: Recompute measure on indirect dependency update
Task: 5349782
1 parent 0674451 commit 1851586

File tree

6 files changed

+140
-38
lines changed

6 files changed

+140
-38
lines changed

src/functions/helper_lookup.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { isZoneInside, positionToZone, zoneToXc } from "../helpers";
22
import { _t } from "../translation";
33
import { EvalContext, FunctionResultObject, Getters, Maybe, Range, UID } from "../types";
44
import { CircularDependencyError, EvaluationError, InvalidReferenceError } from "../types/errors";
5-
import { PivotCoreDefinition, PivotCoreMeasure } from "../types/pivot";
5+
import { PivotCoreMeasure } from "../types/pivot";
66

77
/**
88
* Get the pivot ID from the formula pivot ID.
@@ -37,11 +37,12 @@ export function assertDomainLength(domain: Maybe<FunctionResultObject>[]) {
3737

3838
export function addPivotDependencies(
3939
evalContext: EvalContext,
40-
coreDefinition: PivotCoreDefinition,
40+
pivotId: UID,
4141
forMeasures: PivotCoreMeasure[]
4242
) {
4343
//TODO This function can be very costly when used with PIVOT.VALUE and PIVOT.HEADER
4444
const dependencies: Range[] = [];
45+
const coreDefinition = evalContext.getters.getPivotCoreDefinition(pivotId);
4546

4647
if (coreDefinition.type === "SPREADSHEET" && coreDefinition.dataSet) {
4748
const { sheetId, zone } = coreDefinition.dataSet;
@@ -62,8 +63,7 @@ export function addPivotDependencies(
6263

6364
for (const measure of forMeasures) {
6465
if (measure.computedBy) {
65-
const formula = evalContext.getters.getMeasureCompiledFormula(measure);
66-
dependencies.push(...formula.dependencies.filter((range) => !range.invalidXc));
66+
dependencies.push(...evalContext.getters.getMeasureFullDependencies(pivotId, measure));
6767
}
6868
}
6969
const originPosition = evalContext.__originCellPosition;

src/functions/module_lookup.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,7 @@ export const PIVOT_VALUE = {
780780

781781
addPivotDependencies(
782782
this,
783-
coreDefinition,
783+
pivotId,
784784
coreDefinition.measures.filter((m) => m.id === _measure)
785785
);
786786
pivot.init({ reload: pivot.needsReevaluation });
@@ -824,8 +824,7 @@ export const PIVOT_HEADER = {
824824
const _pivotId = getPivotId(_pivotFormulaId, this.getters);
825825
assertDomainLength(domainArgs);
826826
const pivot = this.getters.getPivot(_pivotId);
827-
const coreDefinition = this.getters.getPivotCoreDefinition(_pivotId);
828-
addPivotDependencies(this, coreDefinition, []);
827+
addPivotDependencies(this, _pivotId, []);
829828
pivot.init({ reload: pivot.needsReevaluation });
830829
const error = pivot.assertIsValid({ throwOnError: false });
831830
if (error) {
@@ -894,7 +893,7 @@ export const PIVOT = {
894893
const pivotId = getPivotId(_pivotFormulaId, this.getters);
895894
const pivot = this.getters.getPivot(pivotId);
896895
const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
897-
addPivotDependencies(this, coreDefinition, coreDefinition.measures);
896+
addPivotDependencies(this, pivotId, coreDefinition.measures);
898897
pivot.init({ reload: pivot.needsReevaluation });
899898
const error = pivot.assertIsValid({ throwOnError: false });
900899
if (error) {

src/helpers/pivot/pivot_presentation.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
isMatrix,
1515
} from "../../types";
1616
import { CellErrorType, NotAvailableError } from "../../types/errors";
17+
import { UID } from "../../types/misc";
1718
import { deepEquals, removeDuplicates, transpose2dPOJO } from "../misc";
1819
import {
1920
NEXT_VALUE,
@@ -50,6 +51,7 @@ type DomainGroups<T> = { [colDomain: string]: { [rowDomain: string]: T } };
5051
export default function (PivotClass: PivotUIConstructor) {
5152
class PivotPresentationLayer extends PivotClass {
5253
private getters: Getters;
54+
private pivotId: UID;
5355
private cache: Record<string, FunctionResultObject> = {};
5456
private rankAsc: CacheForMeasureAndField<DomainGroups<number> | undefined> = {};
5557
private rankDesc: CacheForMeasureAndField<DomainGroups<number> | undefined> = {};
@@ -60,9 +62,10 @@ export default function (PivotClass: PivotUIConstructor) {
6062
DomainGroups<number | undefined> | undefined
6163
> = {};
6264

63-
constructor(custom: ModelConfig["custom"], params: PivotParams) {
65+
constructor(pivotId, custom: ModelConfig["custom"], params: PivotParams) {
6466
super(custom, params);
6567
this.getters = params.getters;
68+
this.pivotId = pivotId;
6669
}
6770

6871
markAsDirtyForEvaluation(): void {
@@ -117,7 +120,7 @@ export default function (PivotClass: PivotUIConstructor) {
117120
return handleError(error, measure.aggregator.toUpperCase());
118121
}
119122
}
120-
const formula = this.getters.getMeasureCompiledFormula(measure);
123+
const formula = this.getters.getMeasureCompiledFormula(this.pivotId, measure);
121124
const getSymbolValue = (symbolName: string) => {
122125
const { columns, rows } = this.definition;
123126
if (columns.find((col) => col.nameWithGranularity === symbolName)) {

src/plugins/core/pivot.ts

Lines changed: 96 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { compile } from "../../formulas";
2-
import { deepCopy, deepEquals } from "../../helpers";
2+
import { deepCopy, deepEquals, getCanonicalSymbolName } from "../../helpers";
33
import { createPivotFormula, getMaxObjectId } from "../../helpers/pivot/pivot_helpers";
44
import { pivotRegistry } from "../../helpers/pivot/pivot_registry";
55
import { SpreadsheetPivotTable } from "../../helpers/pivot/table_spreadsheet_pivot";
@@ -22,11 +22,16 @@ interface Pivot {
2222
formulaId: string;
2323
}
2424

25+
interface MeasureState {
26+
formula: RangeCompiledFormula;
27+
fullDependencies: Range[];
28+
}
29+
2530
interface CoreState {
2631
nextFormulaId: number;
2732
pivots: Record<UID, Pivot | undefined>;
2833
formulaIds: Record<UID, string | undefined>;
29-
compiledMeasureFormulas: Record<UID, Record<string, RangeCompiledFormula | undefined>>;
34+
compiledMeasureFormulas: Record<UID, Record<string, MeasureState | undefined>>;
3035
}
3136

3237
export class PivotCorePlugin extends CorePlugin<CoreState> implements CoreState {
@@ -39,14 +44,15 @@ export class PivotCorePlugin extends CorePlugin<CoreState> implements CoreState
3944
"getMeasureCompiledFormula",
4045
"getPivotName",
4146
"isExistingPivot",
47+
"getMeasureFullDependencies",
4248
] as const;
4349

4450
readonly nextFormulaId: number = 1;
4551
public readonly pivots: {
4652
[pivotId: UID]: Pivot | undefined;
4753
} = {};
4854
public readonly formulaIds: { [formulaId: UID]: UID | undefined } = {};
49-
public readonly compiledMeasureFormulas: Record<UID, Record<string, RangeCompiledFormula>> = {};
55+
public readonly compiledMeasureFormulas: Record<UID, Record<string, MeasureState>> = {};
5056

5157
allowDispatch(cmd: CoreCommand) {
5258
switch (cmd.type) {
@@ -126,7 +132,7 @@ export class PivotCorePlugin extends CorePlugin<CoreState> implements CoreState
126132
}
127133
case "UPDATE_PIVOT": {
128134
this.history.update("pivots", cmd.pivotId, "definition", deepCopy(cmd.pivot));
129-
this.compileCalculatedMeasures(cmd.pivot.measures);
135+
this.compileCalculatedMeasures(cmd.pivotId, cmd.pivot.measures);
130136
break;
131137
}
132138
}
@@ -145,9 +151,15 @@ export class PivotCorePlugin extends CorePlugin<CoreState> implements CoreState
145151
this.history.update("pivots", pivotId, "definition", newDefinition);
146152
}
147153
}
148-
for (const sheetId in this.compiledMeasureFormulas) {
149-
for (const formulaString in this.compiledMeasureFormulas[sheetId]) {
150-
const compiledFormula = this.compiledMeasureFormulas[sheetId][formulaString];
154+
for (const pivotId in this.compiledMeasureFormulas) {
155+
for (const measureId in this.compiledMeasureFormulas[pivotId]) {
156+
// const compiledFormula = this.compiledMeasureFormulas[sheetId][formulaString];
157+
const measure = this.pivots[pivotId]?.definition.measures.find((m) => m.id === measureId);
158+
if (!measure || !measure.computedBy) {
159+
continue;
160+
}
161+
const sheetId = measure.computedBy.sheetId;
162+
const compiledFormula = this.compiledMeasureFormulas[pivotId][measureId].formula;
151163
const newDependencies: Range[] = [];
152164
for (const range of compiledFormula.dependencies) {
153165
const change = applyChange(range);
@@ -162,8 +174,9 @@ export class PivotCorePlugin extends CorePlugin<CoreState> implements CoreState
162174
compiledFormula.tokens,
163175
newDependencies
164176
);
165-
if (newFormulaString !== formulaString) {
166-
this.replaceMeasureFormula(sheetId, formulaString, newFormulaString);
177+
const oldFormulaString = measure.computedBy.formula;
178+
if (newFormulaString !== oldFormulaString) {
179+
this.replaceMeasureFormula(oldFormulaString, newFormulaString);
167180
}
168181
}
169182
}
@@ -210,12 +223,18 @@ export class PivotCorePlugin extends CorePlugin<CoreState> implements CoreState
210223
return pivotId in this.pivots;
211224
}
212225

213-
getMeasureCompiledFormula(measure: PivotCoreMeasure): RangeCompiledFormula {
226+
getMeasureCompiledFormula(pivotId: UID, measure: PivotCoreMeasure): RangeCompiledFormula {
214227
if (!measure.computedBy) {
215228
throw new Error(`Measure ${measure.fieldName} is not computed by formula`);
216229
}
217-
const sheetId = measure.computedBy.sheetId;
218-
return this.compiledMeasureFormulas[sheetId][measure.computedBy.formula];
230+
return this.compiledMeasureFormulas[pivotId][measure.id].formula;
231+
}
232+
233+
getMeasureFullDependencies(pivotId: UID, measure: PivotCoreMeasure): Range[] {
234+
if (!measure.computedBy) {
235+
throw new Error(`Measure ${measure.fieldName} is not computed by formula`);
236+
}
237+
return this.compiledMeasureFormulas[pivotId][measure.id].fullDependencies;
219238
}
220239

221240
// -------------------------------------------------------------------------
@@ -228,27 +247,73 @@ export class PivotCorePlugin extends CorePlugin<CoreState> implements CoreState
228247
formulaId = this.nextFormulaId.toString()
229248
) {
230249
this.history.update("pivots", pivotId, { definition: deepCopy(pivot), formulaId });
231-
this.compileCalculatedMeasures(pivot.measures);
250+
this.compileCalculatedMeasures(pivotId, pivot.measures);
232251
this.history.update("formulaIds", formulaId, pivotId);
233252
this.history.update("nextFormulaId", this.nextFormulaId + 1);
234253
}
235254

236-
private compileCalculatedMeasures(measures: PivotCoreMeasure[]) {
255+
private compileCalculatedMeasures(pivotId: UID, measures: PivotCoreMeasure[]) {
237256
for (const measure of measures) {
238257
if (measure.computedBy) {
239-
const sheetId = measure.computedBy.sheetId;
240258
const compiledFormula = this.compileMeasureFormula(
241259
measure.computedBy.sheetId,
242260
measure.computedBy.formula
243261
);
244262
this.history.update(
245263
"compiledMeasureFormulas",
246-
sheetId,
247-
measure.computedBy.formula,
264+
pivotId,
265+
measure.id,
266+
"formula",
248267
compiledFormula
249268
);
250269
}
251270
}
271+
for (const measure of measures) {
272+
if (measure.computedBy) {
273+
const dependencies = this.computeMeasureFullDependencies(pivotId, measure);
274+
this.history.update(
275+
"compiledMeasureFormulas",
276+
pivotId,
277+
measure.id,
278+
"fullDependencies",
279+
dependencies
280+
);
281+
}
282+
}
283+
}
284+
285+
private computeMeasureFullDependencies(
286+
pivotId: UID,
287+
measure: PivotCoreMeasure,
288+
exploredMeasures: Set<string> = new Set()
289+
): Range[] {
290+
const rangeList: Range[] = [];
291+
const definition = this.getPivotCoreDefinition(pivotId);
292+
const formula = this.getMeasureCompiledFormula(pivotId, measure);
293+
for (const token of formula.tokens) {
294+
if (token.type !== "SYMBOL") {
295+
continue;
296+
}
297+
const existingMeasure = definition.measures.find(
298+
(measureCandidate) =>
299+
getCanonicalSymbolName(measureCandidate.id) === token.value &&
300+
measure.id !== measureCandidate.id
301+
);
302+
303+
if (
304+
!existingMeasure ||
305+
exploredMeasures.has(existingMeasure.id) ||
306+
!existingMeasure.computedBy
307+
)
308+
continue;
309+
310+
rangeList.push(
311+
...this.computeMeasureFullDependencies(pivotId, existingMeasure, exploredMeasures)
312+
);
313+
}
314+
rangeList.push(...formula.dependencies.filter((range) => !range.invalidXc));
315+
exploredMeasures.add(measure.id);
316+
return rangeList;
252317
}
253318

254319
private insertPivot(position: CellPosition, formulaId: UID, table: SpreadsheetPivotTable) {
@@ -314,22 +379,23 @@ export class PivotCorePlugin extends CorePlugin<CoreState> implements CoreState
314379
};
315380
}
316381

317-
private replaceMeasureFormula(sheetId: UID, formulaString: string, newFormulaString: string) {
318-
this.history.update("compiledMeasureFormulas", sheetId, formulaString, undefined);
319-
this.history.update(
320-
"compiledMeasureFormulas",
321-
sheetId,
322-
newFormulaString,
323-
this.compileMeasureFormula(sheetId, newFormulaString)
324-
);
382+
private replaceMeasureFormula(formulaString: string, newFormulaString: string) {
383+
// this.history.update("compiledMeasureFormulas", sheetId, formulaString, undefined);
384+
// this.history.update(
385+
// "compiledMeasureFormulas",
386+
// sheetId,
387+
// newFormulaString,
388+
// this.compileMeasureFormula(sheetId, newFormulaString)
389+
// );
325390
for (const pivotId in this.pivots) {
326391
const pivot = this.pivots[pivotId];
327392
if (!pivot) {
328393
continue;
329394
}
330-
395+
let shouldUpdateMeasures = false;
331396
for (const measure of pivot.definition.measures) {
332397
if (measure.computedBy?.formula === formulaString) {
398+
shouldUpdateMeasures = true;
333399
const measureIndex = pivot.definition.measures.indexOf(measure);
334400
this.history.update(
335401
"pivots",
@@ -338,10 +404,13 @@ export class PivotCorePlugin extends CorePlugin<CoreState> implements CoreState
338404
"measures",
339405
measureIndex,
340406
"computedBy",
341-
{ formula: newFormulaString, sheetId }
407+
{ formula: newFormulaString, sheetId: measure.computedBy.sheetId }
342408
);
343409
}
344410
}
411+
if (shouldUpdateMeasures) {
412+
this.compileCalculatedMeasures(pivotId, pivot.definition.measures);
413+
}
345414
}
346415
}
347416

src/plugins/ui_core_views/pivot_ui.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ export class PivotUIPlugin extends UIPlugin {
304304
const definition = deepCopy(this.getters.getPivotCoreDefinition(pivotId));
305305
if (!(pivotId in this.pivots)) {
306306
const Pivot = withPivotPresentationLayer(pivotRegistry.get(definition.type).ui);
307-
this.pivots[pivotId] = new Pivot(this.custom, { definition, getters: this.getters });
307+
this.pivots[pivotId] = new Pivot(pivotId, this.custom, { definition, getters: this.getters });
308308
} else if (recreate) {
309309
this.pivots[pivotId].onDefinitionChange(definition);
310310
}

tests/pivots/pivot_calculated_measure.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,3 +1188,34 @@ describe("Pivot calculated measure", () => {
11881188
expect(getEvaluatedCell(model, "A6").value).toEqual(0);
11891189
});
11901190
});
1191+
1192+
test("measure takes indirect dependency into account for recalculation", () => {
1193+
const grid = {
1194+
A1: "Customer",
1195+
A2: "Alice",
1196+
A3: "42",
1197+
A4: '=PIVOT.VALUE(1, "calculated")',
1198+
A5: '=PIVOT.VALUE(1, "basedOnCalculated")',
1199+
};
1200+
const model = createModelFromGrid(grid);
1201+
const sheetId = model.getters.getActiveSheetId();
1202+
addPivot(model, "A1:A2", {
1203+
measures: [
1204+
{
1205+
id: "calculated",
1206+
fieldName: "calculated",
1207+
aggregator: "sum",
1208+
computedBy: { formula: "=A3", sheetId },
1209+
},
1210+
{
1211+
id: "basedOnCalculated",
1212+
fieldName: "basedOnCalculated",
1213+
aggregator: "sum",
1214+
computedBy: { formula: "=calculated", sheetId },
1215+
},
1216+
],
1217+
});
1218+
setCellContent(model, "A3", "43");
1219+
expect(getEvaluatedCell(model, "A4").value).toEqual(43);
1220+
expect(getEvaluatedCell(model, "A5").value).toEqual(43);
1221+
});

0 commit comments

Comments
 (0)