Skip to content

Commit 67fec55

Browse files
committed
[IMP] spreadsheet: add named ranges feature
This commits adds the named ranges feature, that allows to defined a custom name for a range of cells, and use that name in formulas. closes #7605 Task: 5380498 Signed-off-by: Lucas Lefèvre (lul) <lul@odoo.com>
1 parent d3546ed commit 67fec55

File tree

75 files changed

+2748
-321
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+2748
-321
lines changed

demo/data.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ export const demoData = {
4848
C5: "42",
4949
C7: "3",
5050
C9: "= SUM( C4:C5 )",
51-
C10: "=SUM(C4:C7)",
52-
C11: "=-(3 + C7 *SUM(C4:C7))",
51+
C10: "=SUM(MyNamedRange)",
52+
C11: "=-(3 + C7 *SUM(MyNamedRange))",
5353
C12: "=SUM(C9:C11)",
5454
C14: "=C14",
5555
C15: "=(+",
@@ -3902,6 +3902,9 @@ export const demoData = {
39023902
},
39033903
pivotNextId: 2,
39043904
customTableStyles: {},
3905+
namedRanges: {
3906+
MyNamedRange: "Sheet1!C4:C7",
3907+
},
39053908
};
39063909

39073910
// Performance dataset

packages/o-spreadsheet-engine/src/collaborative/ot/ot.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { getAddHeaderStartIndex, isDefined } from "../../helpers/misc";
2-
import { getRangeAdapter, rangeAdapterRegistry } from "../../helpers/range";
2+
import {
3+
getIdentityRangeAdapter,
4+
getNamedRangeAdapter,
5+
getRangeAdapter,
6+
rangeAdapterRegistry,
7+
} from "../../helpers/range";
38
import {
49
moveHeaderIndexesOnHeaderAddition,
510
moveHeaderIndexesOnHeaderDeletion,
@@ -77,9 +82,12 @@ function adaptTransform(toTransform: CoreCommand, executed: CoreCommand): CoreCo
7782
if (!adaptFn) {
7883
return toTransform;
7984
}
80-
const rangeAdapter = getRangeAdapter(executed);
81-
if (rangeAdapter) {
82-
return adaptFn(toTransform, rangeAdapter);
85+
let rangeAdapter = getRangeAdapter(executed);
86+
let namedRangeAdapter = getNamedRangeAdapter(executed);
87+
if (rangeAdapter || namedRangeAdapter) {
88+
rangeAdapter = rangeAdapter || getIdentityRangeAdapter();
89+
namedRangeAdapter = namedRangeAdapter || ((name) => name);
90+
return adaptFn(toTransform, rangeAdapter, namedRangeAdapter);
8391
}
8492
return toTransform;
8593
}

packages/o-spreadsheet-engine/src/collaborative/ot/ot_specific.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
CreateTableCommand,
1616
DeleteChartCommand,
1717
DeleteFigureCommand,
18+
DeleteNamedRangeCommand,
1819
DeleteSheetCommand,
1920
DuplicatePivotCommand,
2021
FoldHeaderGroupCommand,
@@ -33,6 +34,7 @@ import {
3334
UpdateCarouselCommand,
3435
UpdateChartCommand,
3536
UpdateFigureCommand,
37+
UpdateNamedRangeCommand,
3638
UpdatePivotCommand,
3739
UpdateTableCommand,
3840
} from "../../types/commands";
@@ -107,6 +109,12 @@ otRegistry.addTransformation(
107109
pivotZoneTransformation
108110
);
109111

112+
otRegistry.addTransformation(
113+
"UPDATE_NAMED_RANGE",
114+
["UPDATE_NAMED_RANGE", "DELETE_NAMED_RANGE"],
115+
updateNamedRangeTransformation
116+
);
117+
110118
function pivotZoneTransformation(
111119
toTransform: AddPivotCommand | UpdatePivotCommand,
112120
executed: AddColumnsRowsCommand | RemoveColumnsRowsCommand
@@ -348,3 +356,22 @@ function groupHeadersTransformation(
348356

349357
return { ...toTransform, start: Math.min(...results), end: Math.max(...results) };
350358
}
359+
360+
function updateNamedRangeTransformation(
361+
toTransform: UpdateNamedRangeCommand | DeleteNamedRangeCommand,
362+
executed: UpdateNamedRangeCommand
363+
): UpdateNamedRangeCommand | DeleteNamedRangeCommand | undefined {
364+
if (executed.newRangeName === executed.oldRangeName) {
365+
return toTransform;
366+
}
367+
if (toTransform.type === "DELETE_NAMED_RANGE" && toTransform.name === executed.oldRangeName) {
368+
return { ...toTransform, name: executed.newRangeName };
369+
}
370+
if (
371+
toTransform.type === "UPDATE_NAMED_RANGE" &&
372+
toTransform.oldRangeName === executed.oldRangeName
373+
) {
374+
return { ...toTransform, oldRangeName: executed.newRangeName };
375+
}
376+
return toTransform;
377+
}

packages/o-spreadsheet-engine/src/collaborative/ot/srt_specific.ts

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { transformDefinition } from "../../helpers/figures/charts/chart_factory";
2-
import { adaptFormulaStringRanges, adaptStringRange } from "../../helpers/formulas";
2+
import { adaptFormulaString, adaptStringRange } from "../../helpers/formulas";
33
import { deepCopy } from "../../helpers/misc";
44
import { specificRangeTransformRegistry } from "../../registries/srt_registry";
55
import {
@@ -11,42 +11,52 @@ import {
1111
UpdateChartCommand,
1212
UpdatePivotCommand,
1313
} from "../../types/commands";
14-
import { RangeAdapter } from "../../types/misc";
14+
import { ApplyRenameNamedRange, RangeAdapter } from "../../types/misc";
1515

1616
function updateCellCommandAdaptRange(
1717
cmd: UpdateCellCommand,
18-
applyChange: RangeAdapter
18+
applyChange: RangeAdapter,
19+
namedRangeAdapter: ApplyRenameNamedRange
1920
): UpdateCellCommand {
20-
const content = cmd.content && adaptFormulaStringRanges(cmd.sheetId, cmd.content, applyChange);
21+
const content =
22+
cmd.content && adaptFormulaString(cmd.sheetId, cmd.content, applyChange, namedRangeAdapter);
2123
return { ...cmd, content };
2224
}
2325
specificRangeTransformRegistry.add("UPDATE_CELL", updateCellCommandAdaptRange);
2426

2527
function addConditionalFormatCommandAdaptRange(
2628
cmd: AddConditionalFormatCommand,
27-
applyChange: RangeAdapter
29+
applyChange: RangeAdapter,
30+
namedRangeAdapter: ApplyRenameNamedRange
2831
): AddConditionalFormatCommand {
2932
const rule = cmd.cf.rule;
3033
cmd = { ...cmd, cf: { ...cmd.cf } };
3134
if (rule.type === "CellIsRule") {
3235
cmd.cf.rule = {
3336
...rule,
34-
values: rule.values.map((val) => adaptFormulaStringRanges(cmd.sheetId, val, applyChange)),
37+
values: rule.values.map((val) =>
38+
adaptFormulaString(cmd.sheetId, val, applyChange, namedRangeAdapter)
39+
),
3540
};
3641
} else if (rule.type === "ColorScaleRule") {
3742
const { minimum: min, maximum: max, midpoint: mid } = rule;
3843
cmd.cf.rule = {
3944
...rule,
4045
minimum: {
4146
...min,
42-
value: min.value && adaptFormulaStringRanges(cmd.sheetId, min.value, applyChange),
47+
value:
48+
min.value && adaptFormulaString(cmd.sheetId, min.value, applyChange, namedRangeAdapter),
4349
},
4450
maximum: {
4551
...max,
46-
value: max.value && adaptFormulaStringRanges(cmd.sheetId, max.value, applyChange),
52+
value:
53+
max.value && adaptFormulaString(cmd.sheetId, max.value, applyChange, namedRangeAdapter),
4754
},
4855
midpoint: mid
49-
? { ...mid, value: adaptFormulaStringRanges(cmd.sheetId, mid.value, applyChange) }
56+
? {
57+
...mid,
58+
value: adaptFormulaString(cmd.sheetId, mid.value, applyChange, namedRangeAdapter),
59+
}
5060
: undefined,
5161
};
5262
} else if (rule.type === "IconSetRule") {
@@ -55,11 +65,11 @@ function addConditionalFormatCommandAdaptRange(
5565
...rule,
5666
upperInflectionPoint: {
5767
...uip,
58-
value: adaptFormulaStringRanges(cmd.sheetId, uip.value, applyChange),
68+
value: adaptFormulaString(cmd.sheetId, uip.value, applyChange, namedRangeAdapter),
5969
},
6070
lowerInflectionPoint: {
6171
...lip,
62-
value: adaptFormulaStringRanges(cmd.sheetId, lip.value, applyChange),
72+
value: adaptFormulaString(cmd.sheetId, lip.value, applyChange, namedRangeAdapter),
6373
},
6474
};
6575
} else if (rule.type === "DataBarRule") {
@@ -76,27 +86,30 @@ specificRangeTransformRegistry.add("ADD_CONDITIONAL_FORMAT", addConditionalForma
7686

7787
function addDataValidationCommandAdaptRange(
7888
cmd: AddDataValidationCommand,
79-
applyChange: RangeAdapter
89+
applyChange: RangeAdapter,
90+
namedRangeAdapter: ApplyRenameNamedRange
8091
): AddDataValidationCommand {
8192
cmd = { ...cmd, rule: { ...cmd.rule, criterion: { ...cmd.rule.criterion } } };
8293
cmd.rule.criterion.values = cmd.rule.criterion.values.map((val) =>
83-
adaptFormulaStringRanges(cmd.sheetId, val, applyChange)
94+
adaptFormulaString(cmd.sheetId, val, applyChange, namedRangeAdapter)
8495
);
8596
return cmd;
8697
}
8798
specificRangeTransformRegistry.add("ADD_DATA_VALIDATION_RULE", addDataValidationCommandAdaptRange);
8899

89100
function addPivotCommandAdaptRange<Cmd extends AddPivotCommand | UpdatePivotCommand>(
90101
cmd: Cmd,
91-
applyChange: RangeAdapter
102+
applyChange: RangeAdapter,
103+
namedRangeAdapter: ApplyRenameNamedRange
92104
): Cmd {
93105
cmd = deepCopy(cmd);
94106
cmd.pivot.measures.map((measure) => {
95107
if (measure.computedBy) {
96-
measure.computedBy.formula = adaptFormulaStringRanges(
108+
measure.computedBy.formula = adaptFormulaString(
97109
measure.computedBy.sheetId,
98110
measure.computedBy.formula,
99-
applyChange
111+
applyChange,
112+
namedRangeAdapter
100113
);
101114
}
102115
});
@@ -110,10 +123,11 @@ specificRangeTransformRegistry.add("UPDATE_CHART", updateChartRangesTransformati
110123

111124
function updateChartRangesTransformation<Cmd extends UpdateChartCommand | CreateChartCommand>(
112125
cmd: Cmd,
113-
applyChange: RangeAdapter
126+
applyChange: RangeAdapter,
127+
namedRangeAdapter: ApplyRenameNamedRange
114128
): Cmd {
115129
return {
116130
...cmd,
117-
definition: transformDefinition(cmd.sheetId, cmd.definition, applyChange),
131+
definition: transformDefinition(cmd.sheetId, cmd.definition, applyChange, namedRangeAdapter),
118132
};
119133
}

packages/o-spreadsheet-engine/src/formulas/compiler.ts

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import { argTargeting } from "../functions/arguments";
22
import { functionRegistry } from "../functions/function_registry";
33
import { concat, parseNumber, unquote } from "../helpers";
4+
import { canBeNamedRangeToken } from "../helpers/formulas";
45
import { _t } from "../translation";
56
import { CoreGetters } from "../types/core_getters";
67
import { BadExpressionError, UnknownFunctionError } from "../types/errors";
78
import { DEFAULT_LOCALE } from "../types/locale";
8-
import { FormulaToExecute, LiteralValues, UID } from "../types/misc";
9+
import {
10+
ApplyRangeChange,
11+
ApplyRenameNamedRange,
12+
FormulaToExecute,
13+
LiteralValues,
14+
NamedRange,
15+
UID,
16+
} from "../types/misc";
917
import { Range, RangeStringOptions } from "../types/range";
1018
import { FunctionCode, FunctionCodeBuilder, Scope } from "./code_builder";
1119
import { AST, ASTFuncall, iterateAstNodes, parseTokens } from "./parser";
@@ -135,6 +143,19 @@ export class CompiledFormula implements Omit<ICompiledFormula, "tokens" | "depen
135143
);
136144
}
137145

146+
getNamedRangesInFormula(getters: CoreGetters): NamedRange[] {
147+
const namedRanges: NamedRange[] = [];
148+
for (let i = 0; i < this.tokens.length; i++) {
149+
if (canBeNamedRangeToken(this.tokens, i)) {
150+
const namedRange = getters.getNamedRange(this.tokens[i].value);
151+
if (namedRange) {
152+
namedRanges.push(namedRange);
153+
}
154+
}
155+
}
156+
return namedRanges;
157+
}
158+
138159
usesSymbol(symbol: string) {
139160
return this.symbols.some((s) => collator.compare(s, symbol) === 0);
140161
}
@@ -174,6 +195,64 @@ export class CompiledFormula implements Omit<ICompiledFormula, "tokens" | "depen
174195
);
175196
}
176197

198+
adaptCompiledFormula(
199+
applyChange: ApplyRangeChange,
200+
applyUpdateNamedRange: ApplyRenameNamedRange
201+
): CompiledFormula {
202+
const newDependencies: Range[] = [];
203+
let hasChanges = false;
204+
for (const range of this.rangeDependencies) {
205+
const change = applyChange(range);
206+
newDependencies.push(change.range);
207+
if (change.changeType !== "NONE") {
208+
hasChanges = true;
209+
}
210+
}
211+
212+
const tokenChanges = this.renameNamedRangeTokens(applyUpdateNamedRange);
213+
214+
if (hasChanges || tokenChanges) {
215+
return new CompiledFormula(
216+
this.sheetId,
217+
tokenChanges?.newTokens || this.tokens,
218+
this.literalValues,
219+
tokenChanges?.newSymbols || this.symbols,
220+
newDependencies,
221+
this.isBadExpression,
222+
compilationCacheKey(tokenChanges?.newTokens || this.tokens),
223+
this.execute
224+
);
225+
}
226+
return this;
227+
}
228+
229+
/** Change the symbols and tokens on a named range change. Return undefined if nothing has changed. */
230+
private renameNamedRangeTokens(
231+
applyUpdateNamedRange: ApplyRenameNamedRange
232+
): { newSymbols: string[]; newTokens: Token[] } | undefined {
233+
let hasChanged = false;
234+
const newTokens: Token[] = [];
235+
const newSymbols: string[] = [];
236+
for (let i = 0; i < this.tokens.length; i++) {
237+
let newToken = this.tokens[i];
238+
239+
if (canBeNamedRangeToken(this.tokens, i)) {
240+
const newName = applyUpdateNamedRange(this.tokens[i].value);
241+
if (newName !== this.tokens[i].value) {
242+
hasChanged = true;
243+
newToken = { ...this.tokens[i], value: newName };
244+
}
245+
}
246+
247+
newTokens.push(newToken);
248+
if (newToken.type === "SYMBOL") {
249+
newSymbols.push(newToken.value);
250+
}
251+
}
252+
253+
return hasChanged ? { newSymbols, newTokens } : undefined;
254+
}
255+
177256
static IsBadExpression(formula: string): boolean {
178257
const tokens = rangeTokenize(formula);
179258
const compiledFormula = compileTokens(tokens);
@@ -450,7 +529,9 @@ function compileTokensOrThrow(tokens: Token[]): ICompiledFormula {
450529
}
451530
case "SYMBOL":
452531
const symbolIndex = symbols.indexOf(ast.value);
453-
return code.return(`getSymbolValue(this.symbols[${symbolIndex}])`);
532+
return code.return(
533+
`getSymbolValue(this.symbols[${symbolIndex}], ${hasRange}, ${isMeta})`
534+
);
454535
case "EMPTY":
455536
return code.return("undefined");
456537
}

packages/o-spreadsheet-engine/src/formulas/parser.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,7 @@ function parseOperand(tokens: TokenList): AST {
189189
case "SYMBOL":
190190
const value = current.value;
191191
const nextToken = tokens.current;
192-
if (
193-
nextToken?.type === "LEFT_PAREN" &&
194-
functionRegex.test(current.value) &&
195-
value === unquote(value, "'")
196-
) {
192+
if (isFuncallToken(current, nextToken)) {
197193
const { args, rightParen } = parseFunctionArgs(tokens);
198194
return {
199195
type: "FUNCALL",
@@ -471,3 +467,11 @@ export function mapAst<T extends AST["type"]>(
471467
return ast;
472468
}
473469
}
470+
471+
export function isFuncallToken(currentToken: Token, nextToken: Token | undefined) {
472+
return (
473+
nextToken?.type === "LEFT_PAREN" &&
474+
functionRegex.test(currentToken.value) &&
475+
currentToken.value === unquote(currentToken.value, "'")
476+
);
477+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,10 @@ export const INDIRECT: AddFunctionDescription = {
336336
this.updateDependencies?.(originPosition);
337337
}
338338

339-
const range = this.getters.getRangeFromSheetXC(sheetId, _reference);
339+
const namedRange = this.getters.getNamedRange(_reference);
340+
const range = namedRange
341+
? namedRange.range
342+
: this.getters.getRangeFromSheetXC(sheetId, _reference);
340343
if (range === undefined || range.invalidXc || range.invalidSheetName) {
341344
return new InvalidReferenceError();
342345
}

0 commit comments

Comments
 (0)