@odoo/o-spreadsheet 16.4.0-alpha.0 → 16.4.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/o-spreadsheet.cjs.js +1131 -487
- package/dist/o-spreadsheet.d.ts +57 -57
- package/dist/o-spreadsheet.esm.js +1131 -487
- package/dist/o-spreadsheet.iife.js +1131 -487
- package/dist/o-spreadsheet.iife.min.js +269 -257
- package/package.json +1 -1
|
@@ -1117,6 +1117,21 @@ function isConsecutive(iterable) {
|
|
|
1117
1117
|
}
|
|
1118
1118
|
}
|
|
1119
1119
|
return true;
|
|
1120
|
+
}
|
|
1121
|
+
class JetSet extends Set {
|
|
1122
|
+
add(...iterable) {
|
|
1123
|
+
for (const element of iterable) {
|
|
1124
|
+
super.add(element);
|
|
1125
|
+
}
|
|
1126
|
+
return this;
|
|
1127
|
+
}
|
|
1128
|
+
delete(...iterable) {
|
|
1129
|
+
let deleted = false;
|
|
1130
|
+
for (const element of iterable) {
|
|
1131
|
+
deleted ||= super.delete(element);
|
|
1132
|
+
}
|
|
1133
|
+
return deleted;
|
|
1134
|
+
}
|
|
1120
1135
|
}
|
|
1121
1136
|
|
|
1122
1137
|
const RBA_REGEX = /rgba?\(|\s+|\)/gi;
|
|
@@ -2887,6 +2902,14 @@ function positions(zone) {
|
|
|
2887
2902
|
}
|
|
2888
2903
|
return positions;
|
|
2889
2904
|
}
|
|
2905
|
+
function forEachPositionsInZone(zone, callback) {
|
|
2906
|
+
const { left, right, top, bottom } = zone;
|
|
2907
|
+
for (let col = left; col <= right; col++) {
|
|
2908
|
+
for (let row = top; row <= bottom; row++) {
|
|
2909
|
+
callback(col, row);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2890
2913
|
/**
|
|
2891
2914
|
* This function returns a zone with coordinates modified according to the change
|
|
2892
2915
|
* applied to the zone. It may be possible to change the zone by resizing or moving
|
|
@@ -4187,6 +4210,11 @@ const invalidateEvaluationCommands = new Set([
|
|
|
4187
4210
|
"INSERT_CELL",
|
|
4188
4211
|
"UNDO",
|
|
4189
4212
|
"REDO",
|
|
4213
|
+
"ADD_MERGE",
|
|
4214
|
+
]);
|
|
4215
|
+
const invalidateDependenciesCommands = new Set([
|
|
4216
|
+
...invalidateEvaluationCommands,
|
|
4217
|
+
"MOVE_RANGES",
|
|
4190
4218
|
]);
|
|
4191
4219
|
const invalidateCFEvaluationCommands = new Set([
|
|
4192
4220
|
...invalidateEvaluationCommands,
|
|
@@ -4404,6 +4432,9 @@ exports.CommandResult = void 0;
|
|
|
4404
4432
|
})(exports.CommandResult || (exports.CommandResult = {}));
|
|
4405
4433
|
|
|
4406
4434
|
const borderStyles = ["thin", "medium", "thick", "dashed", "dotted"];
|
|
4435
|
+
function isMatrix(x) {
|
|
4436
|
+
return Array.isArray(x) && Array.isArray(x[0]);
|
|
4437
|
+
}
|
|
4407
4438
|
var DIRECTION;
|
|
4408
4439
|
(function (DIRECTION) {
|
|
4409
4440
|
DIRECTION["UP"] = "up";
|
|
@@ -10369,7 +10400,7 @@ const PRODUCT = {
|
|
|
10369
10400
|
],
|
|
10370
10401
|
returns: ["NUMBER"],
|
|
10371
10402
|
computeFormat: (factor1) => {
|
|
10372
|
-
return Array.isArray(factor1) ? factor1[0][0]
|
|
10403
|
+
return Array.isArray(factor1?.format) ? factor1.format[0][0] : factor1?.format;
|
|
10373
10404
|
},
|
|
10374
10405
|
compute: function (...factors) {
|
|
10375
10406
|
let count = 0;
|
|
@@ -10595,7 +10626,7 @@ const SUM = {
|
|
|
10595
10626
|
],
|
|
10596
10627
|
returns: ["NUMBER"],
|
|
10597
10628
|
computeFormat: (value1) => {
|
|
10598
|
-
return Array.isArray(value1) ? value1[0][0]
|
|
10629
|
+
return Array.isArray(value1?.format) ? value1.format[0][0] : value1?.format;
|
|
10599
10630
|
},
|
|
10600
10631
|
compute: function (...values) {
|
|
10601
10632
|
return reduceNumbers(values, (acc, a) => acc + a, 0);
|
|
@@ -10867,7 +10898,7 @@ const AVERAGE = {
|
|
|
10867
10898
|
],
|
|
10868
10899
|
returns: ["NUMBER"],
|
|
10869
10900
|
computeFormat: (value1) => {
|
|
10870
|
-
return Array.isArray(value1) ? value1[0][0]
|
|
10901
|
+
return Array.isArray(value1?.format) ? value1.format[0][0] : value1?.format;
|
|
10871
10902
|
},
|
|
10872
10903
|
compute: function (...values) {
|
|
10873
10904
|
let count = 0;
|
|
@@ -10895,7 +10926,7 @@ const AVERAGE_WEIGHTED = {
|
|
|
10895
10926
|
],
|
|
10896
10927
|
returns: ["NUMBER"],
|
|
10897
10928
|
computeFormat: (values) => {
|
|
10898
|
-
return Array.isArray(values) ? values[0][0]
|
|
10929
|
+
return Array.isArray(values?.format) ? values.format[0][0] : values?.format;
|
|
10899
10930
|
},
|
|
10900
10931
|
compute: function (...values) {
|
|
10901
10932
|
let sum = 0;
|
|
@@ -10953,7 +10984,7 @@ const AVERAGEA = {
|
|
|
10953
10984
|
],
|
|
10954
10985
|
returns: ["NUMBER"],
|
|
10955
10986
|
computeFormat: (value1) => {
|
|
10956
|
-
return Array.isArray(value1) ? value1[0][0]
|
|
10987
|
+
return Array.isArray(value1?.format) ? value1.format[0][0] : value1?.format;
|
|
10957
10988
|
},
|
|
10958
10989
|
compute: function (...values) {
|
|
10959
10990
|
let count = 0;
|
|
@@ -11126,7 +11157,7 @@ const LARGE = {
|
|
|
11126
11157
|
],
|
|
11127
11158
|
returns: ["NUMBER"],
|
|
11128
11159
|
computeFormat: (data) => {
|
|
11129
|
-
return Array.isArray(data) ? data[0][0]
|
|
11160
|
+
return Array.isArray(data?.format) ? data.format[0][0] : data?.format;
|
|
11130
11161
|
},
|
|
11131
11162
|
compute: function (data, n) {
|
|
11132
11163
|
const _n = Math.trunc(toNumber(n));
|
|
@@ -11162,7 +11193,7 @@ const MAX = {
|
|
|
11162
11193
|
],
|
|
11163
11194
|
returns: ["NUMBER"],
|
|
11164
11195
|
computeFormat: (value1) => {
|
|
11165
|
-
return Array.isArray(value1) ? value1[0][0]
|
|
11196
|
+
return Array.isArray(value1?.format) ? value1.format[0][0] : value1?.format;
|
|
11166
11197
|
},
|
|
11167
11198
|
compute: function (...values) {
|
|
11168
11199
|
const result = reduceNumbers(values, (acc, a) => (acc < a ? a : acc), -Infinity);
|
|
@@ -11181,7 +11212,7 @@ const MAXA = {
|
|
|
11181
11212
|
],
|
|
11182
11213
|
returns: ["NUMBER"],
|
|
11183
11214
|
computeFormat: (value1) => {
|
|
11184
|
-
return Array.isArray(value1) ? value1[0][0]
|
|
11215
|
+
return Array.isArray(value1?.format) ? value1.format[0][0] : value1?.format;
|
|
11185
11216
|
},
|
|
11186
11217
|
compute: function (...values) {
|
|
11187
11218
|
const maxa = reduceNumbersTextAs0(values, (acc, a) => {
|
|
@@ -11227,7 +11258,7 @@ const MEDIAN = {
|
|
|
11227
11258
|
],
|
|
11228
11259
|
returns: ["NUMBER"],
|
|
11229
11260
|
computeFormat: (value1) => {
|
|
11230
|
-
return Array.isArray(value1) ? value1[0][0]
|
|
11261
|
+
return Array.isArray(value1?.format) ? value1.format[0][0] : value1?.format;
|
|
11231
11262
|
},
|
|
11232
11263
|
compute: function (...values) {
|
|
11233
11264
|
let data = [];
|
|
@@ -11249,7 +11280,7 @@ const MIN = {
|
|
|
11249
11280
|
],
|
|
11250
11281
|
returns: ["NUMBER"],
|
|
11251
11282
|
computeFormat: (value1) => {
|
|
11252
|
-
return Array.isArray(value1) ? value1[0][0]
|
|
11283
|
+
return Array.isArray(value1?.format) ? value1.format[0][0] : value1?.format;
|
|
11253
11284
|
},
|
|
11254
11285
|
compute: function (...values) {
|
|
11255
11286
|
const result = reduceNumbers(values, (acc, a) => (a < acc ? a : acc), Infinity);
|
|
@@ -11268,7 +11299,7 @@ const MINA = {
|
|
|
11268
11299
|
],
|
|
11269
11300
|
returns: ["NUMBER"],
|
|
11270
11301
|
computeFormat: (value1) => {
|
|
11271
|
-
return Array.isArray(value1) ? value1[0][0]
|
|
11302
|
+
return Array.isArray(value1?.format) ? value1.format[0][0] : value1?.format;
|
|
11272
11303
|
},
|
|
11273
11304
|
compute: function (...values) {
|
|
11274
11305
|
const mina = reduceNumbersTextAs0(values, (acc, a) => {
|
|
@@ -11314,7 +11345,7 @@ const PERCENTILE = {
|
|
|
11314
11345
|
],
|
|
11315
11346
|
returns: ["NUMBER"],
|
|
11316
11347
|
computeFormat: (data) => {
|
|
11317
|
-
return Array.isArray(data) ? data[0][0]
|
|
11348
|
+
return Array.isArray(data?.format) ? data.format[0][0] : data?.format;
|
|
11318
11349
|
},
|
|
11319
11350
|
compute: function (data, percentile) {
|
|
11320
11351
|
return PERCENTILE_INC.compute(data, percentile);
|
|
@@ -11332,7 +11363,7 @@ const PERCENTILE_EXC = {
|
|
|
11332
11363
|
],
|
|
11333
11364
|
returns: ["NUMBER"],
|
|
11334
11365
|
computeFormat: (data) => {
|
|
11335
|
-
return Array.isArray(data) ? data[0][0]
|
|
11366
|
+
return Array.isArray(data?.format) ? data.format[0][0] : data?.format;
|
|
11336
11367
|
},
|
|
11337
11368
|
compute: function (data, percentile) {
|
|
11338
11369
|
return centile([data], percentile, false);
|
|
@@ -11350,7 +11381,7 @@ const PERCENTILE_INC = {
|
|
|
11350
11381
|
],
|
|
11351
11382
|
returns: ["NUMBER"],
|
|
11352
11383
|
computeFormat: (data) => {
|
|
11353
|
-
return Array.isArray(data) ? data[0][0]
|
|
11384
|
+
return Array.isArray(data?.format) ? data.format[0][0] : data?.format;
|
|
11354
11385
|
},
|
|
11355
11386
|
compute: function (data, percentile) {
|
|
11356
11387
|
return centile([data], percentile, true);
|
|
@@ -11368,7 +11399,7 @@ const QUARTILE = {
|
|
|
11368
11399
|
],
|
|
11369
11400
|
returns: ["NUMBER"],
|
|
11370
11401
|
computeFormat: (data) => {
|
|
11371
|
-
return Array.isArray(data) ? data[0][0]
|
|
11402
|
+
return Array.isArray(data?.format) ? data.format[0][0] : data?.format;
|
|
11372
11403
|
},
|
|
11373
11404
|
compute: function (data, quartileNumber) {
|
|
11374
11405
|
return QUARTILE_INC.compute(data, quartileNumber);
|
|
@@ -11386,7 +11417,7 @@ const QUARTILE_EXC = {
|
|
|
11386
11417
|
],
|
|
11387
11418
|
returns: ["NUMBER"],
|
|
11388
11419
|
computeFormat: (data) => {
|
|
11389
|
-
return Array.isArray(data) ? data[0][0]
|
|
11420
|
+
return Array.isArray(data?.format) ? data.format[0][0] : data?.format;
|
|
11390
11421
|
},
|
|
11391
11422
|
compute: function (data, quartileNumber) {
|
|
11392
11423
|
const _quartileNumber = Math.trunc(toNumber(quartileNumber));
|
|
@@ -11405,7 +11436,7 @@ const QUARTILE_INC = {
|
|
|
11405
11436
|
],
|
|
11406
11437
|
returns: ["NUMBER"],
|
|
11407
11438
|
computeFormat: (data) => {
|
|
11408
|
-
return Array.isArray(data) ? data[0][0]
|
|
11439
|
+
return Array.isArray(data?.format) ? data.format[0][0] : data?.format;
|
|
11409
11440
|
},
|
|
11410
11441
|
compute: function (data, quartileNumber) {
|
|
11411
11442
|
const _quartileNumber = Math.trunc(toNumber(quartileNumber));
|
|
@@ -11424,7 +11455,7 @@ const SMALL = {
|
|
|
11424
11455
|
],
|
|
11425
11456
|
returns: ["NUMBER"],
|
|
11426
11457
|
computeFormat: (data) => {
|
|
11427
|
-
return Array.isArray(data) ? data[0][0]
|
|
11458
|
+
return Array.isArray(data?.format) ? data.format[0][0] : data?.format;
|
|
11428
11459
|
},
|
|
11429
11460
|
compute: function (data, n) {
|
|
11430
11461
|
const _n = Math.trunc(toNumber(n));
|
|
@@ -16211,17 +16242,11 @@ function extractArgValuesFromArgs(args) {
|
|
|
16211
16242
|
return undefined;
|
|
16212
16243
|
}
|
|
16213
16244
|
if (typeof arg === "function") {
|
|
16214
|
-
return () =>
|
|
16245
|
+
return () => arg()?.value;
|
|
16215
16246
|
}
|
|
16216
|
-
return
|
|
16247
|
+
return arg.value;
|
|
16217
16248
|
});
|
|
16218
16249
|
}
|
|
16219
|
-
function _extractArgValuesFromArgs(arg) {
|
|
16220
|
-
if (Array.isArray(arg)) {
|
|
16221
|
-
return arg.map((col) => col.map((simpleArg) => simpleArg?.value));
|
|
16222
|
-
}
|
|
16223
|
-
return arg?.value;
|
|
16224
|
-
}
|
|
16225
16250
|
const functionRegistry = new FunctionRegistry();
|
|
16226
16251
|
for (let category of categories) {
|
|
16227
16252
|
const fns = category.functions;
|
|
@@ -16873,6 +16898,7 @@ const unFreezePane = {
|
|
|
16873
16898
|
execute: (env) => env.model.dispatch("UNFREEZE_COLUMNS_ROWS", {
|
|
16874
16899
|
sheetId: env.model.getters.getActiveSheetId(),
|
|
16875
16900
|
}),
|
|
16901
|
+
icon: "o-spreadsheet-Icon.UNFREEZE",
|
|
16876
16902
|
};
|
|
16877
16903
|
const freezePane = {
|
|
16878
16904
|
name: _lt("Freeze"),
|
|
@@ -17944,6 +17970,18 @@ css /* scss */ `
|
|
|
17944
17970
|
grid-template-columns: repeat(${ITEMS_PER_LINE}, 1fr);
|
|
17945
17971
|
grid-gap: ${ITEM_HORIZONTAL_MARGIN * 2}px;
|
|
17946
17972
|
}
|
|
17973
|
+
.o-color-picker-toggler-button {
|
|
17974
|
+
display: flex;
|
|
17975
|
+
.o-color-picker-toggler-sign {
|
|
17976
|
+
margin: auto auto;
|
|
17977
|
+
width: 55%;
|
|
17978
|
+
height: 55%;
|
|
17979
|
+
.o-icon {
|
|
17980
|
+
width: 100%;
|
|
17981
|
+
height: 100%;
|
|
17982
|
+
}
|
|
17983
|
+
}
|
|
17984
|
+
}
|
|
17947
17985
|
.o-color-picker-line-item {
|
|
17948
17986
|
width: ${ITEM_EDGE_LENGTH}px;
|
|
17949
17987
|
height: ${ITEM_EDGE_LENGTH}px;
|
|
@@ -20616,7 +20654,14 @@ class ContentEditableHelper {
|
|
|
20616
20654
|
current = it.next();
|
|
20617
20655
|
}
|
|
20618
20656
|
if (current.value !== nodeToFind) {
|
|
20619
|
-
|
|
20657
|
+
/** This situation can happen if the code is called while the selection is not currently on the ContentEditableHelper.
|
|
20658
|
+
* In this case, we return 0 because we don't know the size of the text before the selection.
|
|
20659
|
+
*
|
|
20660
|
+
* A known occurence is triggered since the introduction of commit d4663158 (PR #2038).
|
|
20661
|
+
*
|
|
20662
|
+
* FIXME: find a way to test eventhough the selection API is not available in jsDOM.
|
|
20663
|
+
*/
|
|
20664
|
+
return 0;
|
|
20620
20665
|
}
|
|
20621
20666
|
else {
|
|
20622
20667
|
if (!current.value.hasChildNodes()) {
|
|
@@ -20741,7 +20786,7 @@ FunctionDescriptionProvider.props = {
|
|
|
20741
20786
|
argToFocus: Number,
|
|
20742
20787
|
};
|
|
20743
20788
|
|
|
20744
|
-
const functions$
|
|
20789
|
+
const functions$3 = functionRegistry.content;
|
|
20745
20790
|
const ASSISTANT_WIDTH = 300;
|
|
20746
20791
|
const selectionIndicatorClass = "selector-flag";
|
|
20747
20792
|
const selectionIndicatorColor = "#a9a9a9";
|
|
@@ -21243,7 +21288,7 @@ class Composer extends owl.Component {
|
|
|
21243
21288
|
// initialize Formula Assistant
|
|
21244
21289
|
const tokenContext = tokenAtCursor.functionContext;
|
|
21245
21290
|
const parentFunction = tokenContext.parent.toUpperCase();
|
|
21246
|
-
const description = functions$
|
|
21291
|
+
const description = functions$3[parentFunction];
|
|
21247
21292
|
const argPosition = tokenContext.argPosition;
|
|
21248
21293
|
this.functionDescriptionState.functionName = parentFunction;
|
|
21249
21294
|
this.functionDescriptionState.functionDescription = description;
|
|
@@ -22114,6 +22159,21 @@ FiguresContainer.props = {
|
|
|
22114
22159
|
onFigureDeleted: Function,
|
|
22115
22160
|
};
|
|
22116
22161
|
|
|
22162
|
+
/**
|
|
22163
|
+
* Manages an event listener on a ref. Useful for hooks that want to manage
|
|
22164
|
+
* event listeners, especially more than one. Prefer using t-on directly in
|
|
22165
|
+
* components. If your hook only needs a single event listener, consider simply
|
|
22166
|
+
* returning it from the hook and letting the user attach it with t-on.
|
|
22167
|
+
*
|
|
22168
|
+
* Adapted from Odoo Community - See https://github.com/odoo/odoo/blob/saas-16.2/addons/web/static/src/core/utils/hooks.js
|
|
22169
|
+
*/
|
|
22170
|
+
function useRefListener(ref, ...listener) {
|
|
22171
|
+
owl.useEffect((el) => {
|
|
22172
|
+
el?.addEventListener(...listener);
|
|
22173
|
+
return () => el?.removeEventListener(...listener);
|
|
22174
|
+
}, () => [ref.el]);
|
|
22175
|
+
}
|
|
22176
|
+
|
|
22117
22177
|
/**
|
|
22118
22178
|
* Repeatedly calls a callback function with a time delay between calls.
|
|
22119
22179
|
*/
|
|
@@ -22176,20 +22236,10 @@ function useCellHovered(env, gridRef, callback) {
|
|
|
22176
22236
|
setPosition(undefined, undefined);
|
|
22177
22237
|
}
|
|
22178
22238
|
}
|
|
22179
|
-
|
|
22180
|
-
|
|
22181
|
-
|
|
22182
|
-
|
|
22183
|
-
grid.addEventListener("mouseenter", resume);
|
|
22184
|
-
grid.addEventListener("mousedown", recompute);
|
|
22185
|
-
});
|
|
22186
|
-
owl.onWillUnmount(() => {
|
|
22187
|
-
const grid = gridRef.el;
|
|
22188
|
-
grid.removeEventListener("mousemove", updateMousePosition);
|
|
22189
|
-
grid.removeEventListener("mouseleave", pause);
|
|
22190
|
-
grid.removeEventListener("mouseenter", resume);
|
|
22191
|
-
grid.removeEventListener("mousedown", recompute);
|
|
22192
|
-
});
|
|
22239
|
+
useRefListener(gridRef, "mousemove", updateMousePosition);
|
|
22240
|
+
useRefListener(gridRef, "mouseleave", pause);
|
|
22241
|
+
useRefListener(gridRef, "mouseenter", resume);
|
|
22242
|
+
useRefListener(gridRef, "mousedown", recompute);
|
|
22193
22243
|
function setPosition(col, row) {
|
|
22194
22244
|
if (col !== hoveredPosition.col || row !== hoveredPosition.row) {
|
|
22195
22245
|
hoveredPosition.col = col;
|
|
@@ -22228,16 +22278,9 @@ function useTouchMove(gridRef, handler, canMoveUp) {
|
|
|
22228
22278
|
x = currentX;
|
|
22229
22279
|
y = currentY;
|
|
22230
22280
|
}
|
|
22231
|
-
|
|
22232
|
-
|
|
22233
|
-
|
|
22234
|
-
gridRef.el.addEventListener("touchmove", onTouchMove);
|
|
22235
|
-
});
|
|
22236
|
-
owl.onWillUnmount(() => {
|
|
22237
|
-
gridRef.el.removeEventListener("touchstart", onTouchStart);
|
|
22238
|
-
gridRef.el.removeEventListener("touchend", onTouchEnd);
|
|
22239
|
-
gridRef.el.removeEventListener("touchmove", onTouchMove);
|
|
22240
|
-
});
|
|
22281
|
+
useRefListener(gridRef, "touchstart", onTouchStart);
|
|
22282
|
+
useRefListener(gridRef, "touchend", onTouchEnd);
|
|
22283
|
+
useRefListener(gridRef, "touchmove", onTouchMove);
|
|
22241
22284
|
}
|
|
22242
22285
|
class GridOverlay extends owl.Component {
|
|
22243
22286
|
static template = "o-spreadsheet-GridOverlay";
|
|
@@ -25088,7 +25131,7 @@ function indentCode(code) {
|
|
|
25088
25131
|
* is useful for the composer, which needs to be able to work with incomplete
|
|
25089
25132
|
* formulas.
|
|
25090
25133
|
*/
|
|
25091
|
-
const functions$
|
|
25134
|
+
const functions$2 = functionRegistry.content;
|
|
25092
25135
|
const POSTFIX_UNARY_OPERATORS = ["%"];
|
|
25093
25136
|
const OPERATORS = "+,-,*,/,:,=,<>,>=,>,<=,<,^,&".split(",").concat(POSTFIX_UNARY_OPERATORS);
|
|
25094
25137
|
function tokenize(str) {
|
|
@@ -25211,7 +25254,7 @@ function tokenizeSymbol(chars) {
|
|
|
25211
25254
|
}
|
|
25212
25255
|
if (result.length) {
|
|
25213
25256
|
const value = result;
|
|
25214
|
-
const isFunction = value.toUpperCase() in functions$
|
|
25257
|
+
const isFunction = value.toUpperCase() in functions$2;
|
|
25215
25258
|
if (isFunction) {
|
|
25216
25259
|
return { type: "FUNCTION", value };
|
|
25217
25260
|
}
|
|
@@ -25681,7 +25724,7 @@ function rangeTokenize(formula) {
|
|
|
25681
25724
|
return result;
|
|
25682
25725
|
}
|
|
25683
25726
|
|
|
25684
|
-
const functions = functionRegistry.content;
|
|
25727
|
+
const functions$1 = functionRegistry.content;
|
|
25685
25728
|
const OPERATOR_MAP = {
|
|
25686
25729
|
"=": "EQ",
|
|
25687
25730
|
"+": "ADD",
|
|
@@ -25744,56 +25787,32 @@ function compile(formula) {
|
|
|
25744
25787
|
* between a cell value and a non cell value.
|
|
25745
25788
|
*/
|
|
25746
25789
|
function compileFunctionArgs(ast) {
|
|
25747
|
-
const
|
|
25748
|
-
const
|
|
25749
|
-
|
|
25750
|
-
|
|
25751
|
-
|
|
25752
|
-
|
|
25753
|
-
|
|
25754
|
-
|
|
25755
|
-
|
|
25756
|
-
|
|
25757
|
-
|
|
25758
|
-
|
|
25759
|
-
|
|
25760
|
-
const
|
|
25761
|
-
|
|
25762
|
-
|
|
25763
|
-
|
|
25764
|
-
|
|
25765
|
-
|
|
25766
|
-
for (let i = 0; i < nbrArg; i++) {
|
|
25767
|
-
const argPosition = functionDefinition.getArgToFocus(i + 1) - 1;
|
|
25768
|
-
if (0 <= argPosition && argPosition < functionDefinition.args.length) {
|
|
25769
|
-
const currentArg = currentFunctionArguments[i];
|
|
25770
|
-
const argDefinition = functionDefinition.args[argPosition];
|
|
25771
|
-
const argTypes = argDefinition.type || [];
|
|
25772
|
-
// detect when an argument need to be evaluated as a meta argument
|
|
25773
|
-
const isMeta = argTypes.includes("META");
|
|
25774
|
-
// detect when an argument need to be evaluated as a lazy argument
|
|
25775
|
-
const isLazy = argDefinition.lazy;
|
|
25776
|
-
const hasRange = argTypes.some((t) => t === "RANGE" ||
|
|
25777
|
-
t === "RANGE<BOOLEAN>" ||
|
|
25778
|
-
t === "RANGE<DATE>" ||
|
|
25779
|
-
t === "RANGE<NUMBER>" ||
|
|
25780
|
-
t === "RANGE<STRING>");
|
|
25781
|
-
const isRangeOnly = argTypes.every((t) => t === "RANGE" ||
|
|
25782
|
-
t === "RANGE<BOOLEAN>" ||
|
|
25783
|
-
t === "RANGE<DATE>" ||
|
|
25784
|
-
t === "RANGE<NUMBER>" ||
|
|
25785
|
-
t === "RANGE<STRING>");
|
|
25786
|
-
if (isRangeOnly) {
|
|
25787
|
-
if (currentArg.type !== "REFERENCE") {
|
|
25788
|
-
throw new BadExpressionError(_lt("Function %s expects the parameter %s to be reference to a cell or range, not a %s.", ast.value.toUpperCase(), (i + 1).toString(), currentArg.type.toLowerCase()));
|
|
25789
|
-
}
|
|
25790
|
+
const { args } = ast;
|
|
25791
|
+
const functionName = ast.value.toUpperCase();
|
|
25792
|
+
const functionDefinition = functions$1[functionName];
|
|
25793
|
+
assertEnoughArgs(ast);
|
|
25794
|
+
const compiledArgs = [];
|
|
25795
|
+
for (let i = 0; i < args.length; i++) {
|
|
25796
|
+
const argToFocus = functionDefinition.getArgToFocus(i + 1) - 1;
|
|
25797
|
+
const argDefinition = functionDefinition.args[argToFocus];
|
|
25798
|
+
const currentArg = args[i];
|
|
25799
|
+
const argTypes = argDefinition.type || [];
|
|
25800
|
+
// detect when an argument need to be evaluated as a meta argument
|
|
25801
|
+
const isMeta = argTypes.includes("META");
|
|
25802
|
+
// detect when an argument need to be evaluated as a lazy argument
|
|
25803
|
+
const isLazy = argDefinition.lazy;
|
|
25804
|
+
const hasRange = argTypes.some((t) => isRangeType(t));
|
|
25805
|
+
const isRangeOnly = argTypes.every((t) => isRangeType(t));
|
|
25806
|
+
if (isRangeOnly) {
|
|
25807
|
+
if (!isRangeInput(currentArg)) {
|
|
25808
|
+
throw new BadExpressionError(_lt("Function %s expects the parameter %s to be reference to a cell or range, not a %s.", functionName, (i + 1).toString(), currentArg.type.toLowerCase()));
|
|
25790
25809
|
}
|
|
25791
|
-
const compiledAST = compileAST(currentArg, isMeta, hasRange, {
|
|
25792
|
-
functionName: ast.value.toUpperCase(),
|
|
25793
|
-
paramIndex: i + 1,
|
|
25794
|
-
});
|
|
25795
|
-
compiledArgs.push(isLazy ? compiledAST.wrapInClosure() : compiledAST);
|
|
25796
25810
|
}
|
|
25811
|
+
const compiledAST = compileAST(currentArg, isMeta, hasRange, {
|
|
25812
|
+
functionName,
|
|
25813
|
+
paramIndex: i + 1,
|
|
25814
|
+
});
|
|
25815
|
+
compiledArgs.push(isLazy ? compiledAST.wrapInClosure() : compiledAST);
|
|
25797
25816
|
}
|
|
25798
25817
|
return compiledArgs;
|
|
25799
25818
|
}
|
|
@@ -25937,6 +25956,41 @@ function formulaArguments(tokens) {
|
|
|
25937
25956
|
dependencies,
|
|
25938
25957
|
constantValues,
|
|
25939
25958
|
};
|
|
25959
|
+
}
|
|
25960
|
+
/**
|
|
25961
|
+
* Check if arguments are supplied in the correct quantities
|
|
25962
|
+
*/
|
|
25963
|
+
function assertEnoughArgs(ast) {
|
|
25964
|
+
const nbrArg = ast.args.length;
|
|
25965
|
+
const functionName = ast.value.toUpperCase();
|
|
25966
|
+
const functionDefinition = functions$1[functionName];
|
|
25967
|
+
if (nbrArg < functionDefinition.minArgRequired) {
|
|
25968
|
+
throw new BadExpressionError(_lt("Invalid number of arguments for the %s function. Expected %s minimum, but got %s instead.", functionName, functionDefinition.minArgRequired.toString(), nbrArg.toString()));
|
|
25969
|
+
}
|
|
25970
|
+
if (nbrArg > functionDefinition.maxArgPossible) {
|
|
25971
|
+
throw new BadExpressionError(_lt("Invalid number of arguments for the %s function. Expected %s maximum, but got %s instead.", functionName, functionDefinition.maxArgPossible.toString(), nbrArg.toString()));
|
|
25972
|
+
}
|
|
25973
|
+
const repeatableArgs = functionDefinition.nbrArgRepeating;
|
|
25974
|
+
if (repeatableArgs > 1) {
|
|
25975
|
+
const unrepeatableArgs = functionDefinition.args.length - repeatableArgs;
|
|
25976
|
+
const repeatingArgs = nbrArg - unrepeatableArgs;
|
|
25977
|
+
if (repeatingArgs % repeatableArgs !== 0) {
|
|
25978
|
+
throw new BadExpressionError(_lt("Invalid number of arguments for the %s function. Expected all arguments after position %s to be supplied by groups of %s arguments", functionName, unrepeatableArgs.toString(), repeatableArgs.toString()));
|
|
25979
|
+
}
|
|
25980
|
+
}
|
|
25981
|
+
}
|
|
25982
|
+
function isRangeType(type) {
|
|
25983
|
+
return type.startsWith("RANGE");
|
|
25984
|
+
}
|
|
25985
|
+
function isRangeInput(arg) {
|
|
25986
|
+
if (arg.type === "REFERENCE") {
|
|
25987
|
+
return true;
|
|
25988
|
+
}
|
|
25989
|
+
if (arg.type === "FUNCALL") {
|
|
25990
|
+
const fnDef = functions$1[arg.value.toUpperCase()];
|
|
25991
|
+
return fnDef && isRangeType(fnDef.returns[0]);
|
|
25992
|
+
}
|
|
25993
|
+
return false;
|
|
25940
25994
|
}
|
|
25941
25995
|
|
|
25942
25996
|
/**
|
|
@@ -26053,16 +26107,6 @@ function convertOperator(operator) {
|
|
|
26053
26107
|
// -------------------------------------
|
|
26054
26108
|
// WORKSHEET HELPERS
|
|
26055
26109
|
// -------------------------------------
|
|
26056
|
-
function getCellType(value) {
|
|
26057
|
-
switch (typeof value) {
|
|
26058
|
-
case "boolean":
|
|
26059
|
-
return "b";
|
|
26060
|
-
case "string":
|
|
26061
|
-
return "str";
|
|
26062
|
-
case "number":
|
|
26063
|
-
return "n";
|
|
26064
|
-
}
|
|
26065
|
-
}
|
|
26066
26110
|
function convertHeightToExcel(height) {
|
|
26067
26111
|
return Math.round(HEIGHT_FACTOR * height * 100) / 100;
|
|
26068
26112
|
}
|
|
@@ -29749,23 +29793,7 @@ class CellPlugin extends CorePlugin {
|
|
|
29749
29793
|
*/
|
|
29750
29794
|
createFormulaCellWithDependencies(id, compiledFormula, format, style, sheetId) {
|
|
29751
29795
|
const dependencies = compiledFormula.dependencies.map((xc) => this.getters.getRangeFromSheetXC(sheetId, xc));
|
|
29752
|
-
|
|
29753
|
-
// Only for formulas with dependencies because
|
|
29754
|
-
// **the closure is expensive memory-wise**
|
|
29755
|
-
return {
|
|
29756
|
-
id,
|
|
29757
|
-
get content() {
|
|
29758
|
-
return buildFormulaContent(sheetId, {
|
|
29759
|
-
dependencies: this.dependencies,
|
|
29760
|
-
compiledFormula: this.compiledFormula,
|
|
29761
|
-
});
|
|
29762
|
-
},
|
|
29763
|
-
style,
|
|
29764
|
-
format,
|
|
29765
|
-
isFormula: true,
|
|
29766
|
-
compiledFormula,
|
|
29767
|
-
dependencies,
|
|
29768
|
-
};
|
|
29796
|
+
return new FormulaCellWithDependencies(id, compiledFormula, format, style, dependencies, sheetId, this.buildFormulaContent.bind(this));
|
|
29769
29797
|
}
|
|
29770
29798
|
createErrorFormula(id, content, format, style, error) {
|
|
29771
29799
|
return {
|
|
@@ -29791,6 +29819,31 @@ class CellPlugin extends CorePlugin {
|
|
|
29791
29819
|
const sheetZone = this.getters.getSheetZone(sheetId);
|
|
29792
29820
|
return isInside(col, row, sheetZone) ? 0 /* CommandResult.Success */ : 18 /* CommandResult.TargetOutOfSheet */;
|
|
29793
29821
|
}
|
|
29822
|
+
}
|
|
29823
|
+
class FormulaCellWithDependencies {
|
|
29824
|
+
id;
|
|
29825
|
+
compiledFormula;
|
|
29826
|
+
format;
|
|
29827
|
+
style;
|
|
29828
|
+
dependencies;
|
|
29829
|
+
sheetId;
|
|
29830
|
+
buildFormulaContent;
|
|
29831
|
+
isFormula = true;
|
|
29832
|
+
constructor(id, compiledFormula, format, style, dependencies, sheetId, buildFormulaContent) {
|
|
29833
|
+
this.id = id;
|
|
29834
|
+
this.compiledFormula = compiledFormula;
|
|
29835
|
+
this.format = format;
|
|
29836
|
+
this.style = style;
|
|
29837
|
+
this.dependencies = dependencies;
|
|
29838
|
+
this.sheetId = sheetId;
|
|
29839
|
+
this.buildFormulaContent = buildFormulaContent;
|
|
29840
|
+
}
|
|
29841
|
+
get content() {
|
|
29842
|
+
return this.buildFormulaContent(this.sheetId, {
|
|
29843
|
+
dependencies: this.dependencies,
|
|
29844
|
+
compiledFormula: this.compiledFormula,
|
|
29845
|
+
});
|
|
29846
|
+
}
|
|
29794
29847
|
}
|
|
29795
29848
|
|
|
29796
29849
|
class ChartPlugin extends CorePlugin {
|
|
@@ -33082,6 +33135,931 @@ class UIPlugin extends BasePlugin {
|
|
|
33082
33135
|
drawGrid(ctx, layer) { }
|
|
33083
33136
|
}
|
|
33084
33137
|
|
|
33138
|
+
const functionMap = functionRegistry.mapping;
|
|
33139
|
+
/**
|
|
33140
|
+
* Return all functions necessary to properly evaluate a formula:
|
|
33141
|
+
* - a refFn function to read any reference, cell or range of a normalized formula
|
|
33142
|
+
* - a range function to convert any reference to a proper value array
|
|
33143
|
+
* - an evaluation context
|
|
33144
|
+
*/
|
|
33145
|
+
function buildCompilationParameters(context, getters, computeCell) {
|
|
33146
|
+
const builder = new CompilationParametersBuilder(context, getters, computeCell);
|
|
33147
|
+
return builder.getParameters();
|
|
33148
|
+
}
|
|
33149
|
+
class CompilationParametersBuilder {
|
|
33150
|
+
getters;
|
|
33151
|
+
computeCell;
|
|
33152
|
+
evalContext;
|
|
33153
|
+
constructor(context, getters, computeCell) {
|
|
33154
|
+
this.getters = getters;
|
|
33155
|
+
this.computeCell = computeCell;
|
|
33156
|
+
this.evalContext = Object.assign(Object.create(functionMap), context, {
|
|
33157
|
+
getters: this.getters,
|
|
33158
|
+
});
|
|
33159
|
+
}
|
|
33160
|
+
getParameters() {
|
|
33161
|
+
return [this.refFn.bind(this), this.range.bind(this), this.evalContext];
|
|
33162
|
+
}
|
|
33163
|
+
/**
|
|
33164
|
+
* Returns the value of the cell(s) used in reference
|
|
33165
|
+
*
|
|
33166
|
+
* @param range the references used
|
|
33167
|
+
* @param isMeta if a reference is supposed to be used in a `meta` parameter as described in the
|
|
33168
|
+
* function for which this parameter is used, we just return the string of the parameter.
|
|
33169
|
+
* The `compute` of the formula's function must process it completely
|
|
33170
|
+
*/
|
|
33171
|
+
refFn(range, isMeta, functionName, paramNumber) {
|
|
33172
|
+
if (isMeta) {
|
|
33173
|
+
// Use zoneToXc of zone instead of getRangeString to avoid sending unbounded ranges
|
|
33174
|
+
return { value: zoneToXc(range.zone) };
|
|
33175
|
+
}
|
|
33176
|
+
if (!isZoneValid(range.zone)) {
|
|
33177
|
+
throw new InvalidReferenceError();
|
|
33178
|
+
}
|
|
33179
|
+
// if the formula definition could have accepted a range, we would pass through the _range function and not here
|
|
33180
|
+
if (range.zone.bottom !== range.zone.top || range.zone.left !== range.zone.right) {
|
|
33181
|
+
throw new Error(paramNumber
|
|
33182
|
+
? _lt("Function %s expects the parameter %s to be a single value or a single cell reference, not a range.", functionName.toString(), paramNumber.toString())
|
|
33183
|
+
: _lt("Function %s expects its parameters to be single values or single cell references, not ranges.", functionName.toString()));
|
|
33184
|
+
}
|
|
33185
|
+
if (range.invalidSheetName) {
|
|
33186
|
+
throw new Error(_lt("Invalid sheet name: %s", range.invalidSheetName));
|
|
33187
|
+
}
|
|
33188
|
+
return this.readCell(range);
|
|
33189
|
+
}
|
|
33190
|
+
readCell(range) {
|
|
33191
|
+
if (!this.getters.tryGetSheet(range.sheetId)) {
|
|
33192
|
+
throw new Error(_lt("Invalid sheet name"));
|
|
33193
|
+
}
|
|
33194
|
+
const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };
|
|
33195
|
+
const evaluatedCell = this.getEvaluatedCellIfNotEmpty(position);
|
|
33196
|
+
if (evaluatedCell === undefined) {
|
|
33197
|
+
return { value: null, format: this.getters.getCell(position)?.format };
|
|
33198
|
+
}
|
|
33199
|
+
return evaluatedCell;
|
|
33200
|
+
}
|
|
33201
|
+
getEvaluatedCellIfNotEmpty(position) {
|
|
33202
|
+
const evaluatedCell = this.getEvaluatedCell(position);
|
|
33203
|
+
if (evaluatedCell.type === CellValueType.empty) {
|
|
33204
|
+
const cell = this.getters.getCell(position);
|
|
33205
|
+
if (!cell || cell.content === "") {
|
|
33206
|
+
return undefined;
|
|
33207
|
+
}
|
|
33208
|
+
}
|
|
33209
|
+
return evaluatedCell;
|
|
33210
|
+
}
|
|
33211
|
+
getEvaluatedCell(position) {
|
|
33212
|
+
const evaluatedCell = this.computeCell(position);
|
|
33213
|
+
if (evaluatedCell.type === CellValueType.error) {
|
|
33214
|
+
throw evaluatedCell.error;
|
|
33215
|
+
}
|
|
33216
|
+
return evaluatedCell;
|
|
33217
|
+
}
|
|
33218
|
+
/**
|
|
33219
|
+
* Return the values of the cell(s) used in reference, but always in the format of a range even
|
|
33220
|
+
* if a single cell is referenced. It is a list of col values. This is useful for the formulas that describe parameters as
|
|
33221
|
+
* range<number> etc.
|
|
33222
|
+
*
|
|
33223
|
+
* Note that each col is possibly sparse: it only contain the values of cells
|
|
33224
|
+
* that are actually present in the grid.
|
|
33225
|
+
*/
|
|
33226
|
+
range({ sheetId, zone }) {
|
|
33227
|
+
if (!isZoneValid(zone)) {
|
|
33228
|
+
throw new InvalidReferenceError();
|
|
33229
|
+
}
|
|
33230
|
+
// Performance issue: Avoid fetching data on positions that are out of the spreadsheet
|
|
33231
|
+
// e.g. A1:ZZZ9999 in a sheet with 10 cols and 10 rows should ignore everything past J10 and return a 10x10 array
|
|
33232
|
+
const sheetZone = this.getters.getSheetZone(sheetId);
|
|
33233
|
+
const _zone = intersection(zone, sheetZone);
|
|
33234
|
+
if (!_zone) {
|
|
33235
|
+
return { value: [[]], format: [[]] };
|
|
33236
|
+
}
|
|
33237
|
+
const height = _zone.bottom - _zone.top + 1;
|
|
33238
|
+
const width = _zone.right - _zone.left + 1;
|
|
33239
|
+
const value = Array.from({ length: width }, () => Array.from({ length: height }));
|
|
33240
|
+
const format = Array.from({ length: width }, () => Array.from({ length: height }));
|
|
33241
|
+
// Performance issue: nested loop is faster than a map here
|
|
33242
|
+
for (let col = _zone.left; col <= _zone.right; col++) {
|
|
33243
|
+
for (let row = _zone.top; row <= _zone.bottom; row++) {
|
|
33244
|
+
const evaluatedCell = this.getEvaluatedCellIfNotEmpty({ sheetId: sheetId, col, row });
|
|
33245
|
+
if (evaluatedCell) {
|
|
33246
|
+
const colIndex = col - _zone.left;
|
|
33247
|
+
const rowIndex = row - _zone.top;
|
|
33248
|
+
value[colIndex][rowIndex] = evaluatedCell.value;
|
|
33249
|
+
if (evaluatedCell.format !== undefined) {
|
|
33250
|
+
format[colIndex][rowIndex] = evaluatedCell.format;
|
|
33251
|
+
}
|
|
33252
|
+
}
|
|
33253
|
+
}
|
|
33254
|
+
}
|
|
33255
|
+
return { value, format };
|
|
33256
|
+
}
|
|
33257
|
+
}
|
|
33258
|
+
|
|
33259
|
+
/**
|
|
33260
|
+
* This class is an implementation of a dependency Graph.
|
|
33261
|
+
* The graph is used to evaluate the cells in the correct
|
|
33262
|
+
* order, and should be updated each time a cell's content is modified
|
|
33263
|
+
*
|
|
33264
|
+
*/
|
|
33265
|
+
class FormulaDependencyGraph {
|
|
33266
|
+
/**
|
|
33267
|
+
* Internal structure:
|
|
33268
|
+
* - key: a cell position (encoded as an integer)
|
|
33269
|
+
* - value: a set of cell positions that depends on the key
|
|
33270
|
+
*
|
|
33271
|
+
* Given
|
|
33272
|
+
* - A1:"= B1 + SQRT(B2)"
|
|
33273
|
+
* - C1:"= B1";
|
|
33274
|
+
* - C2:"= C1"
|
|
33275
|
+
*
|
|
33276
|
+
* we will have something like:
|
|
33277
|
+
* - B1 ---> (A1, C1) meaning A1 and C1 depends on B1
|
|
33278
|
+
* - B2 ---> (A1) meaning A1 depends on B2
|
|
33279
|
+
* - C1 ---> (C2) meaning C2 depends on C1
|
|
33280
|
+
*/
|
|
33281
|
+
inverseDependencies = new Map();
|
|
33282
|
+
dependencies = new Map();
|
|
33283
|
+
removeAllDependencies(formulaPositionId) {
|
|
33284
|
+
const dependencies = this.dependencies.get(formulaPositionId);
|
|
33285
|
+
if (!dependencies) {
|
|
33286
|
+
return;
|
|
33287
|
+
}
|
|
33288
|
+
for (const dependency of dependencies) {
|
|
33289
|
+
this.inverseDependencies.get(dependency)?.delete(formulaPositionId);
|
|
33290
|
+
}
|
|
33291
|
+
this.dependencies.delete(formulaPositionId);
|
|
33292
|
+
}
|
|
33293
|
+
addDependencies(formulaPositionId, dependencies) {
|
|
33294
|
+
for (const dependency of dependencies) {
|
|
33295
|
+
const inverseDependencies = this.inverseDependencies.get(dependency);
|
|
33296
|
+
if (inverseDependencies) {
|
|
33297
|
+
inverseDependencies.add(formulaPositionId);
|
|
33298
|
+
}
|
|
33299
|
+
else {
|
|
33300
|
+
this.inverseDependencies.set(dependency, new Set([formulaPositionId]));
|
|
33301
|
+
}
|
|
33302
|
+
}
|
|
33303
|
+
const existingDependencies = this.dependencies.get(formulaPositionId);
|
|
33304
|
+
if (existingDependencies) {
|
|
33305
|
+
existingDependencies.push(...dependencies);
|
|
33306
|
+
}
|
|
33307
|
+
else {
|
|
33308
|
+
this.dependencies.set(formulaPositionId, dependencies);
|
|
33309
|
+
}
|
|
33310
|
+
}
|
|
33311
|
+
/**
|
|
33312
|
+
* Return the cell and all cells that depend on it,
|
|
33313
|
+
* in the correct order they should be evaluated.
|
|
33314
|
+
* This is called a topological ordering (excluding cycles)
|
|
33315
|
+
*/
|
|
33316
|
+
getCellsDependingOn(positionIds) {
|
|
33317
|
+
const visited = new JetSet();
|
|
33318
|
+
const queue = Array.from(positionIds);
|
|
33319
|
+
while (queue.length > 0) {
|
|
33320
|
+
const node = queue.shift();
|
|
33321
|
+
visited.add(node);
|
|
33322
|
+
const adjacentNodes = this.inverseDependencies.get(node) || new Set();
|
|
33323
|
+
for (const adjacentNode of adjacentNodes) {
|
|
33324
|
+
if (!visited.has(adjacentNode)) {
|
|
33325
|
+
queue.push(adjacentNode);
|
|
33326
|
+
}
|
|
33327
|
+
}
|
|
33328
|
+
}
|
|
33329
|
+
visited.delete(...positionIds);
|
|
33330
|
+
return visited;
|
|
33331
|
+
}
|
|
33332
|
+
}
|
|
33333
|
+
|
|
33334
|
+
/**
|
|
33335
|
+
* Contains, for each cell, the array
|
|
33336
|
+
* formulas that could potentially spread on it
|
|
33337
|
+
* This is essentially a two way mapping between array formulas
|
|
33338
|
+
* and their results.
|
|
33339
|
+
*
|
|
33340
|
+
* As we don't allow two array formulas to spread on the same cell, this structure
|
|
33341
|
+
* is used to force the reevaluation of the potential spreaders of a cell when the
|
|
33342
|
+
* content of this cell is modified. This structure should be updated each time
|
|
33343
|
+
* an array formula is evaluated and try to spread on another cell.
|
|
33344
|
+
*
|
|
33345
|
+
*/
|
|
33346
|
+
class SpreadingRelation {
|
|
33347
|
+
/**
|
|
33348
|
+
* Internal structure:
|
|
33349
|
+
* For something like
|
|
33350
|
+
* - A2:'=SPLIT("KAYAK", "A")'
|
|
33351
|
+
* - B1:'=TRANSPOSE(SPLIT("COYOTE", "O"))'
|
|
33352
|
+
*
|
|
33353
|
+
* Resulting in:
|
|
33354
|
+
* ```
|
|
33355
|
+
* -----------------
|
|
33356
|
+
* | | A | B | C |
|
|
33357
|
+
* |---+---+---+---|
|
|
33358
|
+
* | 1 | | C | |
|
|
33359
|
+
* | 2 | K | Y | K |
|
|
33360
|
+
* | 3 | | T | |
|
|
33361
|
+
* | 4 | | E | |
|
|
33362
|
+
* -----------------
|
|
33363
|
+
* ```
|
|
33364
|
+
* We will have `resultsToArrayFormulas` looking like:
|
|
33365
|
+
* - (B2) --> (A2, B1) meaning B2 can be the result of A2 OR B1
|
|
33366
|
+
* - (C2) --> (A2) meaning C2 is the result of A2
|
|
33367
|
+
* - (B3) --> (B1) meaning B3 is the result of B1
|
|
33368
|
+
* - (B4) --> (B1) meaning B4 is the result of B1
|
|
33369
|
+
*
|
|
33370
|
+
* We will have `arrayFormulasToResults` looking like:
|
|
33371
|
+
* - (A2) --> (B2, C2) meaning A2 spreads on B2 and C2
|
|
33372
|
+
* - (B1) --> (B2, B3, B4) meaning B1 spreads on B2, B3 and B4
|
|
33373
|
+
*
|
|
33374
|
+
*/
|
|
33375
|
+
resultsToArrayFormulas = new Map();
|
|
33376
|
+
arrayFormulasToResults = new Map();
|
|
33377
|
+
getFormulaPositionsSpreadingOn(resultPositionId) {
|
|
33378
|
+
return this.resultsToArrayFormulas.get(resultPositionId) || EMPTY_ARRAY;
|
|
33379
|
+
}
|
|
33380
|
+
getArrayResultPositionIds(formulasPositionId) {
|
|
33381
|
+
return this.arrayFormulasToResults.get(formulasPositionId) || EMPTY_ARRAY;
|
|
33382
|
+
}
|
|
33383
|
+
/**
|
|
33384
|
+
* Remove a node, also remove it from other nodes adjacency list
|
|
33385
|
+
*/
|
|
33386
|
+
removeNode(positionId) {
|
|
33387
|
+
this.resultsToArrayFormulas.delete(positionId);
|
|
33388
|
+
this.arrayFormulasToResults.delete(positionId);
|
|
33389
|
+
}
|
|
33390
|
+
/**
|
|
33391
|
+
* Create a spreading relation between two cells
|
|
33392
|
+
*/
|
|
33393
|
+
addRelation({ arrayFormulaPositionId, resultPositionId, }) {
|
|
33394
|
+
if (!this.resultsToArrayFormulas.has(resultPositionId)) {
|
|
33395
|
+
this.resultsToArrayFormulas.set(resultPositionId, new Set());
|
|
33396
|
+
}
|
|
33397
|
+
this.resultsToArrayFormulas.get(resultPositionId)?.add(arrayFormulaPositionId);
|
|
33398
|
+
if (!this.arrayFormulasToResults.has(arrayFormulaPositionId)) {
|
|
33399
|
+
this.arrayFormulasToResults.set(arrayFormulaPositionId, new Set());
|
|
33400
|
+
}
|
|
33401
|
+
this.arrayFormulasToResults.get(arrayFormulaPositionId)?.add(resultPositionId);
|
|
33402
|
+
}
|
|
33403
|
+
hasArrayFormulaResult(positionId) {
|
|
33404
|
+
return this.resultsToArrayFormulas.has(positionId);
|
|
33405
|
+
}
|
|
33406
|
+
isArrayFormula(positionId) {
|
|
33407
|
+
return this.arrayFormulasToResults.has(positionId);
|
|
33408
|
+
}
|
|
33409
|
+
}
|
|
33410
|
+
const EMPTY_ARRAY = [];
|
|
33411
|
+
|
|
33412
|
+
const MAX_ITERATION = 30;
|
|
33413
|
+
class Evaluator {
|
|
33414
|
+
getters;
|
|
33415
|
+
compilationParams;
|
|
33416
|
+
positionEncoder = new PositionBitsEncoder();
|
|
33417
|
+
evaluatedCells = new Map();
|
|
33418
|
+
formulaDependencies = lazy(new FormulaDependencyGraph());
|
|
33419
|
+
blockedArrayFormulas = new Set();
|
|
33420
|
+
spreadingRelations = new SpreadingRelation();
|
|
33421
|
+
constructor(context, getters) {
|
|
33422
|
+
this.getters = getters;
|
|
33423
|
+
this.compilationParams = buildCompilationParameters(context, getters, (position) => this.computeCell(this.encodePosition(position)));
|
|
33424
|
+
}
|
|
33425
|
+
getEvaluatedCell(position) {
|
|
33426
|
+
return this.evaluatedCells.get(this.encodePosition(position)) || createEvaluatedCell("");
|
|
33427
|
+
}
|
|
33428
|
+
getArrayFormulaSpreadingOn(position) {
|
|
33429
|
+
const positionId = this.encodePosition(position);
|
|
33430
|
+
const formulaPosition = this.getArrayFormulaSpreadingOnId(positionId);
|
|
33431
|
+
return formulaPosition !== undefined ? this.decodePosition(formulaPosition) : undefined;
|
|
33432
|
+
}
|
|
33433
|
+
getEvaluatedPositions() {
|
|
33434
|
+
return [...this.evaluatedCells.keys()].map(this.decodePosition.bind(this));
|
|
33435
|
+
}
|
|
33436
|
+
getArrayFormulaSpreadingOnId(positionId) {
|
|
33437
|
+
if (!this.spreadingRelations.hasArrayFormulaResult(positionId)) {
|
|
33438
|
+
return undefined;
|
|
33439
|
+
}
|
|
33440
|
+
const arrayFormulas = this.spreadingRelations.getFormulaPositionsSpreadingOn(positionId);
|
|
33441
|
+
return Array.from(arrayFormulas).find((positionId) => !this.blockedArrayFormulas.has(positionId));
|
|
33442
|
+
}
|
|
33443
|
+
updateDependencies(position) {
|
|
33444
|
+
const positionId = this.encodePosition(position);
|
|
33445
|
+
this.formulaDependencies().removeAllDependencies(positionId);
|
|
33446
|
+
const dependencies = this.getDirectDependencies(positionId);
|
|
33447
|
+
this.formulaDependencies().addDependencies(positionId, dependencies);
|
|
33448
|
+
}
|
|
33449
|
+
evaluateCells(positions) {
|
|
33450
|
+
const cells = positions.map(this.encodePosition.bind(this));
|
|
33451
|
+
const cellsToCompute = new JetSet(cells);
|
|
33452
|
+
const arrayFormulas = this.getArrayFormulasImpactedByChangesOf(cells);
|
|
33453
|
+
cellsToCompute.add(...this.getCellsDependingOn(cells));
|
|
33454
|
+
cellsToCompute.add(...arrayFormulas);
|
|
33455
|
+
cellsToCompute.add(...this.getCellsDependingOn(arrayFormulas));
|
|
33456
|
+
this.evaluate(cellsToCompute);
|
|
33457
|
+
}
|
|
33458
|
+
getArrayFormulasImpactedByChangesOf(positionIds) {
|
|
33459
|
+
const impactedPositionIds = new JetSet();
|
|
33460
|
+
for (const positionId of positionIds) {
|
|
33461
|
+
const content = this.getCell(positionId)?.content;
|
|
33462
|
+
const arrayFormulaPositionId = this.getArrayFormulaSpreadingOnId(positionId);
|
|
33463
|
+
if (arrayFormulaPositionId !== undefined) {
|
|
33464
|
+
// take into account new collisions.
|
|
33465
|
+
impactedPositionIds.add(arrayFormulaPositionId);
|
|
33466
|
+
}
|
|
33467
|
+
if (!content) {
|
|
33468
|
+
// The previous content could have blocked some array formulas
|
|
33469
|
+
impactedPositionIds.add(...this.getArrayFormulasBlockedByOrSpreadingOn(positionId));
|
|
33470
|
+
}
|
|
33471
|
+
}
|
|
33472
|
+
return impactedPositionIds;
|
|
33473
|
+
}
|
|
33474
|
+
buildDependencyGraph() {
|
|
33475
|
+
this.blockedArrayFormulas = new Set();
|
|
33476
|
+
this.spreadingRelations = new SpreadingRelation();
|
|
33477
|
+
this.formulaDependencies = lazy(() => {
|
|
33478
|
+
const dependencyGraph = new FormulaDependencyGraph();
|
|
33479
|
+
for (const positionId of this.getAllCells()) {
|
|
33480
|
+
const dependencies = this.getDirectDependencies(positionId);
|
|
33481
|
+
dependencyGraph.addDependencies(positionId, dependencies);
|
|
33482
|
+
}
|
|
33483
|
+
return dependencyGraph;
|
|
33484
|
+
});
|
|
33485
|
+
}
|
|
33486
|
+
evaluateAllCells() {
|
|
33487
|
+
this.evaluatedCells = new Map();
|
|
33488
|
+
this.evaluate(this.getAllCells());
|
|
33489
|
+
}
|
|
33490
|
+
getAllCells() {
|
|
33491
|
+
const positionIds = new JetSet();
|
|
33492
|
+
for (const sheetId of this.getters.getSheetIds()) {
|
|
33493
|
+
const cellIds = this.getters.getCells(sheetId);
|
|
33494
|
+
for (const cellId in cellIds) {
|
|
33495
|
+
positionIds.add(this.encodePosition(this.getters.getCellPosition(cellId)));
|
|
33496
|
+
}
|
|
33497
|
+
}
|
|
33498
|
+
return positionIds;
|
|
33499
|
+
}
|
|
33500
|
+
getArrayFormulasBlockedByOrSpreadingOn(positionId) {
|
|
33501
|
+
if (!this.spreadingRelations.hasArrayFormulaResult(positionId)) {
|
|
33502
|
+
return [];
|
|
33503
|
+
}
|
|
33504
|
+
const arrayFormulas = this.spreadingRelations.getFormulaPositionsSpreadingOn(positionId);
|
|
33505
|
+
const cells = new JetSet(arrayFormulas);
|
|
33506
|
+
cells.add(...this.getCellsDependingOn(arrayFormulas));
|
|
33507
|
+
return cells;
|
|
33508
|
+
}
|
|
33509
|
+
nextPositionsToUpdate = new JetSet();
|
|
33510
|
+
cellsBeingComputed = new Set();
|
|
33511
|
+
evaluate(cells) {
|
|
33512
|
+
this.cellsBeingComputed = new Set();
|
|
33513
|
+
this.nextPositionsToUpdate = cells;
|
|
33514
|
+
let currentIteration = 0;
|
|
33515
|
+
while (this.nextPositionsToUpdate.size && currentIteration++ < MAX_ITERATION) {
|
|
33516
|
+
const positionIds = Array.from(this.nextPositionsToUpdate);
|
|
33517
|
+
this.nextPositionsToUpdate.clear();
|
|
33518
|
+
for (let i = 0; i < positionIds.length; ++i) {
|
|
33519
|
+
const cell = positionIds[i];
|
|
33520
|
+
this.evaluatedCells.delete(cell);
|
|
33521
|
+
}
|
|
33522
|
+
for (let i = 0; i < positionIds.length; ++i) {
|
|
33523
|
+
const cell = positionIds[i];
|
|
33524
|
+
this.setEvaluatedCell(cell, this.computeCell(cell));
|
|
33525
|
+
}
|
|
33526
|
+
}
|
|
33527
|
+
}
|
|
33528
|
+
setEvaluatedCell(positionId, evaluatedCell) {
|
|
33529
|
+
if (this.nextPositionsToUpdate.has(positionId)) {
|
|
33530
|
+
this.nextPositionsToUpdate.delete(positionId);
|
|
33531
|
+
}
|
|
33532
|
+
this.evaluatedCells.set(positionId, evaluatedCell);
|
|
33533
|
+
}
|
|
33534
|
+
computeCell(positionId) {
|
|
33535
|
+
const evaluation = this.evaluatedCells.get(positionId);
|
|
33536
|
+
if (evaluation) {
|
|
33537
|
+
return evaluation; // already computed
|
|
33538
|
+
}
|
|
33539
|
+
if (!this.blockedArrayFormulas.has(positionId)) {
|
|
33540
|
+
this.invalidateSpreading(positionId);
|
|
33541
|
+
}
|
|
33542
|
+
const cell = this.getCell(positionId);
|
|
33543
|
+
if (cell === undefined) {
|
|
33544
|
+
return createEvaluatedCell("");
|
|
33545
|
+
}
|
|
33546
|
+
const cellId = cell.id;
|
|
33547
|
+
try {
|
|
33548
|
+
if (this.cellsBeingComputed.has(cellId)) {
|
|
33549
|
+
throw new CircularDependencyError();
|
|
33550
|
+
}
|
|
33551
|
+
this.cellsBeingComputed.add(cellId);
|
|
33552
|
+
return cell.isFormula
|
|
33553
|
+
? this.computeFormulaCell(cell)
|
|
33554
|
+
: evaluateLiteral(cell.content, cell.format);
|
|
33555
|
+
}
|
|
33556
|
+
catch (e) {
|
|
33557
|
+
return this.handleError(e, cell);
|
|
33558
|
+
}
|
|
33559
|
+
finally {
|
|
33560
|
+
this.cellsBeingComputed.delete(cellId);
|
|
33561
|
+
}
|
|
33562
|
+
}
|
|
33563
|
+
handleError(e, cell) {
|
|
33564
|
+
if (!(e instanceof Error)) {
|
|
33565
|
+
e = new Error(e);
|
|
33566
|
+
}
|
|
33567
|
+
const msg = e?.errorType || CellErrorType.GenericError;
|
|
33568
|
+
// apply function name
|
|
33569
|
+
const __lastFnCalled = this.compilationParams[2].__lastFnCalled || "";
|
|
33570
|
+
const error = new EvaluationError(msg, e.message.replace("[[FUNCTION_NAME]]", __lastFnCalled), e.logLevel !== undefined ? e.logLevel : CellErrorLevel.error);
|
|
33571
|
+
return errorCell(cell.content, error);
|
|
33572
|
+
}
|
|
33573
|
+
computeFormulaCell(cellData) {
|
|
33574
|
+
const cellId = cellData.id;
|
|
33575
|
+
this.compilationParams[2].__originCellXC = () => {
|
|
33576
|
+
// compute the value lazily for performance reasons
|
|
33577
|
+
const position = this.compilationParams[2].getters.getCellPosition(cellId);
|
|
33578
|
+
return toXC(position.col, position.row);
|
|
33579
|
+
};
|
|
33580
|
+
const formulaReturn = cellData.compiledFormula.execute(cellData.dependencies, ...this.compilationParams);
|
|
33581
|
+
assertFormulaReturnHasConsistentDimensions(formulaReturn);
|
|
33582
|
+
const { value: computedValue, format: computedFormat } = formulaReturn;
|
|
33583
|
+
if (!isMatrix(computedValue)) {
|
|
33584
|
+
return createEvaluatedCell(computedValue, cellData.format || computedFormat);
|
|
33585
|
+
}
|
|
33586
|
+
const formulaPosition = this.getters.getCellPosition(cellId);
|
|
33587
|
+
this.assertSheetHasEnoughSpaceToSpreadFormulaResult(formulaPosition, computedValue);
|
|
33588
|
+
forEachSpreadPositionInMatrix(computedValue, this.updateSpreadRelation(formulaPosition));
|
|
33589
|
+
forEachSpreadPositionInMatrix(computedValue, this.checkCollision(formulaPosition));
|
|
33590
|
+
forEachSpreadPositionInMatrix(computedValue,
|
|
33591
|
+
// due the isMatrix check above, we know that formulaReturn is MatrixFunctionReturn
|
|
33592
|
+
this.spreadValues(formulaPosition, formulaReturn));
|
|
33593
|
+
const formatFromPosition = formatFromPositionAccess(computedFormat);
|
|
33594
|
+
return createEvaluatedCell(computedValue[0][0], cellData.format || formatFromPosition(0, 0));
|
|
33595
|
+
}
|
|
33596
|
+
assertSheetHasEnoughSpaceToSpreadFormulaResult({ sheetId, col, row }, matrixResult) {
|
|
33597
|
+
const numberOfCols = this.getters.getNumberCols(sheetId);
|
|
33598
|
+
const numberOfRows = this.getters.getNumberRows(sheetId);
|
|
33599
|
+
const enoughCols = col + matrixResult.length <= numberOfCols;
|
|
33600
|
+
const enoughRows = row + matrixResult[0].length <= numberOfRows;
|
|
33601
|
+
if (enoughCols && enoughRows) {
|
|
33602
|
+
return;
|
|
33603
|
+
}
|
|
33604
|
+
if (enoughCols) {
|
|
33605
|
+
throw new Error(_lt("Result couldn't be automatically expanded. Please insert more rows."));
|
|
33606
|
+
}
|
|
33607
|
+
if (enoughRows) {
|
|
33608
|
+
throw new Error(_lt("Result couldn't be automatically expanded. Please insert more columns."));
|
|
33609
|
+
}
|
|
33610
|
+
throw new Error(_lt("Result couldn't be automatically expanded. Please insert more columns and rows."));
|
|
33611
|
+
}
|
|
33612
|
+
updateSpreadRelation({ sheetId, col, row, }) {
|
|
33613
|
+
const arrayFormulaPositionId = this.encodePosition({ sheetId, col, row });
|
|
33614
|
+
return (i, j) => {
|
|
33615
|
+
const position = { sheetId, col: i + col, row: j + row };
|
|
33616
|
+
const resultPositionId = this.encodePosition(position);
|
|
33617
|
+
this.spreadingRelations.addRelation({ resultPositionId, arrayFormulaPositionId });
|
|
33618
|
+
};
|
|
33619
|
+
}
|
|
33620
|
+
checkCollision({ sheetId, col, row }) {
|
|
33621
|
+
const formulaPositionId = this.encodePosition({ sheetId, col, row });
|
|
33622
|
+
return (i, j) => {
|
|
33623
|
+
const position = { sheetId: sheetId, col: i + col, row: j + row };
|
|
33624
|
+
const rawCell = this.getters.getCell(position);
|
|
33625
|
+
if (rawCell?.content ||
|
|
33626
|
+
this.getters.getEvaluatedCell(position).type !== CellValueType.empty) {
|
|
33627
|
+
this.blockedArrayFormulas.add(formulaPositionId);
|
|
33628
|
+
throw new Error(_lt("Array result was not expanded because it would overwrite data in %s.", toXC(position.col, position.row)));
|
|
33629
|
+
}
|
|
33630
|
+
this.blockedArrayFormulas.delete(formulaPositionId);
|
|
33631
|
+
};
|
|
33632
|
+
}
|
|
33633
|
+
spreadValues({ sheetId, col, row }, matrixResult) {
|
|
33634
|
+
const formatFromPosition = formatFromPositionAccess(matrixResult.format);
|
|
33635
|
+
return (i, j) => {
|
|
33636
|
+
const position = { sheetId, col: i + col, row: j + row };
|
|
33637
|
+
const cell = this.getters.getCell(position);
|
|
33638
|
+
const format = cell?.format;
|
|
33639
|
+
const evaluatedCell = createEvaluatedCell(matrixResult.value[i][j], format || formatFromPosition(i, j));
|
|
33640
|
+
const positionId = this.encodePosition(position);
|
|
33641
|
+
this.setEvaluatedCell(positionId, evaluatedCell);
|
|
33642
|
+
// check if formula dependencies present in the spread zone
|
|
33643
|
+
// if so, they need to be recomputed
|
|
33644
|
+
this.nextPositionsToUpdate.add(...this.getCellsDependingOn([positionId]));
|
|
33645
|
+
};
|
|
33646
|
+
}
|
|
33647
|
+
invalidateSpreading(positionId) {
|
|
33648
|
+
if (!this.spreadingRelations.isArrayFormula(positionId)) {
|
|
33649
|
+
return;
|
|
33650
|
+
}
|
|
33651
|
+
for (const child of this.spreadingRelations.getArrayResultPositionIds(positionId)) {
|
|
33652
|
+
const content = this.getCell(child)?.content;
|
|
33653
|
+
if (content) {
|
|
33654
|
+
// there's no point at re-evaluating overlapping array formulas,
|
|
33655
|
+
// there's still a collision
|
|
33656
|
+
continue;
|
|
33657
|
+
}
|
|
33658
|
+
this.evaluatedCells.delete(child);
|
|
33659
|
+
this.nextPositionsToUpdate.add(...this.getCellsDependingOn([child]));
|
|
33660
|
+
this.nextPositionsToUpdate.add(...this.getArrayFormulasBlockedByOrSpreadingOn(child));
|
|
33661
|
+
}
|
|
33662
|
+
this.spreadingRelations.removeNode(positionId);
|
|
33663
|
+
}
|
|
33664
|
+
// ----------------------------------------------------------
|
|
33665
|
+
// COMMON FUNCTIONALITY
|
|
33666
|
+
// ----------------------------------------------------------
|
|
33667
|
+
getDirectDependencies(positionId) {
|
|
33668
|
+
const cell = this.getCell(positionId);
|
|
33669
|
+
if (!cell?.isFormula) {
|
|
33670
|
+
return [];
|
|
33671
|
+
}
|
|
33672
|
+
const dependencies = [];
|
|
33673
|
+
for (const range of cell.dependencies) {
|
|
33674
|
+
if (range.invalidSheetName || range.invalidXc) {
|
|
33675
|
+
continue;
|
|
33676
|
+
}
|
|
33677
|
+
const sheetId = range.sheetId;
|
|
33678
|
+
forEachPositionsInZone(range.zone, (col, row) => {
|
|
33679
|
+
dependencies.push(this.encodePosition({ sheetId, col, row }));
|
|
33680
|
+
});
|
|
33681
|
+
}
|
|
33682
|
+
return dependencies;
|
|
33683
|
+
}
|
|
33684
|
+
getCellsDependingOn(positionIds) {
|
|
33685
|
+
return this.formulaDependencies().getCellsDependingOn(positionIds);
|
|
33686
|
+
}
|
|
33687
|
+
getCell(positionId) {
|
|
33688
|
+
return this.getters.getCell(this.decodePosition(positionId));
|
|
33689
|
+
}
|
|
33690
|
+
encodePosition(position) {
|
|
33691
|
+
return this.positionEncoder.encode(position);
|
|
33692
|
+
}
|
|
33693
|
+
decodePosition(positionId) {
|
|
33694
|
+
return this.positionEncoder.decode(positionId);
|
|
33695
|
+
}
|
|
33696
|
+
}
|
|
33697
|
+
function forEachSpreadPositionInMatrix(matrix, callback) {
|
|
33698
|
+
for (let i = 0; i < matrix.length; ++i) {
|
|
33699
|
+
for (let j = 0; j < matrix[i].length; ++j) {
|
|
33700
|
+
if (i === 0 && j === 0) {
|
|
33701
|
+
continue;
|
|
33702
|
+
}
|
|
33703
|
+
callback(i, j);
|
|
33704
|
+
}
|
|
33705
|
+
}
|
|
33706
|
+
}
|
|
33707
|
+
function formatFromPositionAccess(format) {
|
|
33708
|
+
return isMatrix(format) ? (i, j) => format[i][j] : () => format;
|
|
33709
|
+
}
|
|
33710
|
+
function assertFormulaReturnHasConsistentDimensions(formulaReturn) {
|
|
33711
|
+
const { value: computedValue, format: computedFormat } = formulaReturn;
|
|
33712
|
+
if (!isMatrix(computedValue)) {
|
|
33713
|
+
if (isMatrix(computedFormat)) {
|
|
33714
|
+
throw new Error("A format matrix should never be associated with a scalar value");
|
|
33715
|
+
}
|
|
33716
|
+
return;
|
|
33717
|
+
}
|
|
33718
|
+
if (isMatrix(computedFormat)) {
|
|
33719
|
+
const sameDimensions = computedValue.length === computedFormat.length &&
|
|
33720
|
+
computedValue[0].length === computedFormat[0].length;
|
|
33721
|
+
if (!sameDimensions) {
|
|
33722
|
+
throw new Error("Formats and values should have the same dimensions!");
|
|
33723
|
+
}
|
|
33724
|
+
}
|
|
33725
|
+
}
|
|
33726
|
+
/**
|
|
33727
|
+
* Encode (and decode) cell positions { sheetId, col, row }
|
|
33728
|
+
* to a single integer.
|
|
33729
|
+
*
|
|
33730
|
+
* `col` and `row` values are encoded on 21 bits each (max 2^21 = 2_097_152),
|
|
33731
|
+
* An incremental integer id is assigned to each different sheet id, starting at 0.
|
|
33732
|
+
*
|
|
33733
|
+
* e.g.
|
|
33734
|
+
* Given { col: 10, row: 4, sheetId: "abcde" }
|
|
33735
|
+
* we have:
|
|
33736
|
+
* - row "4" encoded on 21 bits: 000000000000000000100
|
|
33737
|
+
* - col "10" encoded on 21 bits: 000000000000000001010
|
|
33738
|
+
* - sheetId: let's say it's the 4th sheetId met, encoded to: 11
|
|
33739
|
+
*
|
|
33740
|
+
* The final encoded value is found by concatenating the 3 bit sequences:
|
|
33741
|
+
*
|
|
33742
|
+
* sheetId: 11
|
|
33743
|
+
* col: 000000000000000001010
|
|
33744
|
+
* row: 000000000000000000100
|
|
33745
|
+
* => 11000000000000000001010000000000000000000100
|
|
33746
|
+
*
|
|
33747
|
+
* this binary sequence is the integer 13194160504836
|
|
33748
|
+
*/
|
|
33749
|
+
class PositionBitsEncoder {
|
|
33750
|
+
sheetMapping = {};
|
|
33751
|
+
inverseSheetMapping = new Map();
|
|
33752
|
+
constructor() {
|
|
33753
|
+
try {
|
|
33754
|
+
// @ts-ignore
|
|
33755
|
+
o_spreadsheet.__DEBUG__ = o_spreadsheet.__DEBUG__ || {};
|
|
33756
|
+
// @ts-ignore
|
|
33757
|
+
o_spreadsheet.__DEBUG__.decodePosition = this.decode.bind(this);
|
|
33758
|
+
// @ts-ignore
|
|
33759
|
+
o_spreadsheet.__DEBUG__.encodePosition = this.encode.bind(this);
|
|
33760
|
+
}
|
|
33761
|
+
catch (error) { }
|
|
33762
|
+
}
|
|
33763
|
+
/**
|
|
33764
|
+
* Encode a cell position to a single integer.
|
|
33765
|
+
*/
|
|
33766
|
+
encode({ sheetId, col, row }) {
|
|
33767
|
+
return (this.encodeSheet(sheetId) << 42n) | (BigInt(col) << 21n) | BigInt(row);
|
|
33768
|
+
}
|
|
33769
|
+
decode(id) {
|
|
33770
|
+
// keep only the last 21 bits by AND-ing the bit sequence with 21 ones
|
|
33771
|
+
const row = Number(id & 2097151n);
|
|
33772
|
+
const col = Number((id >> 21n) & 2097151n);
|
|
33773
|
+
const sheetId = this.decodeSheet(id >> 42n);
|
|
33774
|
+
return { sheetId, col, row };
|
|
33775
|
+
}
|
|
33776
|
+
encodeSheet(sheetId) {
|
|
33777
|
+
const sheetKey = this.sheetMapping[sheetId];
|
|
33778
|
+
if (sheetKey === undefined) {
|
|
33779
|
+
const newSheetKey = BigInt(Object.keys(this.sheetMapping).length);
|
|
33780
|
+
this.sheetMapping[sheetId] = newSheetKey;
|
|
33781
|
+
this.inverseSheetMapping.set(newSheetKey, sheetId);
|
|
33782
|
+
return newSheetKey;
|
|
33783
|
+
}
|
|
33784
|
+
return sheetKey;
|
|
33785
|
+
}
|
|
33786
|
+
decodeSheet(sheetKey) {
|
|
33787
|
+
const sheetId = this.inverseSheetMapping.get(sheetKey);
|
|
33788
|
+
if (sheetId === undefined) {
|
|
33789
|
+
throw new Error("Sheet id not found");
|
|
33790
|
+
}
|
|
33791
|
+
return sheetId;
|
|
33792
|
+
}
|
|
33793
|
+
}
|
|
33794
|
+
|
|
33795
|
+
const functions = functionRegistry.content;
|
|
33796
|
+
//#region
|
|
33797
|
+
// ---------------------------------------------------------------------------
|
|
33798
|
+
// INTRODUCTION
|
|
33799
|
+
// ---------------------------------------------------------------------------
|
|
33800
|
+
// The evaluation plugin is in charge of computing the values of the cells.
|
|
33801
|
+
// This is a fairly complex task for several reasons:
|
|
33802
|
+
// Reason n°1: Cells can contain formulas that must be interpreted to know
|
|
33803
|
+
// the final value of the cell. And these formulas can depend on other cells.
|
|
33804
|
+
// ex A1:"=SUM(B1:B2)" we have to evaluate B1:B2 first to be able to evaluate A1.
|
|
33805
|
+
// We say here that we have a 'formula dependency' between A1 and B1:B2.
|
|
33806
|
+
// Reason n°2: A cell can assign value to other cells that haven't content.
|
|
33807
|
+
// This concerns cells containing a formula that returns an array of values.
|
|
33808
|
+
// ex A1:"=SPLIT('Odoo','d')" Evaluating A1 must assign the value "O" to A1 and
|
|
33809
|
+
// "oo" to B1. We say here that we have a 'spread relation' between A1 and B1.
|
|
33810
|
+
// B1 have a spread value from A1.
|
|
33811
|
+
// Note that a cell can contain a formula which depends on other cells which
|
|
33812
|
+
// themselves can:
|
|
33813
|
+
// - contain formulas which depends on other cells (and so on).
|
|
33814
|
+
// - contain a spread value from other formulas which depends on other cells
|
|
33815
|
+
// (and so on).
|
|
33816
|
+
// I - How to build the evaluation ?
|
|
33817
|
+
// If we had only formulas dependencies to treat, the evaluation would be
|
|
33818
|
+
// simple: the formulas dependencies are directly deduced from the content
|
|
33819
|
+
// of the formulas. With the dependencies we are able to calculate which
|
|
33820
|
+
// formula must be evaluated before another.
|
|
33821
|
+
// Cycles
|
|
33822
|
+
// ------
|
|
33823
|
+
// We can also easily detect if the cells are included in reference cycles
|
|
33824
|
+
// and return an error in this case. ex: A1:"=B1" B1:"=C1" C1:"=A1"
|
|
33825
|
+
// The "#CYCLE" error must be returned for
|
|
33826
|
+
// all three cells.
|
|
33827
|
+
// But there's more! There are formulas referring to other cells but never
|
|
33828
|
+
// use them. This is the case for example
|
|
33829
|
+
// with the "IF" formula. ex:
|
|
33830
|
+
// A1:"=IF(D1,A2,B1)"
|
|
33831
|
+
// A2:"=A1"
|
|
33832
|
+
// In this case it is obvious that we have a cyclic dependency. But in practice
|
|
33833
|
+
// this will only exist if D1 is true.
|
|
33834
|
+
// For this reason, we believe that the evaluation should be **partly recursive**:
|
|
33835
|
+
// The function computing a formula cell starts by marking them as 'being evaluated'
|
|
33836
|
+
// and then call itself on the dependencies of the concerned cell. This allows
|
|
33837
|
+
// to evaluate the dependencies before the cell itself and to detect
|
|
33838
|
+
// if the cell that is being evaluated isn't part of a cycle.
|
|
33839
|
+
// II - The spread relation anticipation problem
|
|
33840
|
+
// The biggest difficulty to solve with the evaluation lies in the fact that
|
|
33841
|
+
// we cannot anticipate the spread relations: cells impacted by the result array
|
|
33842
|
+
// of a formula are only determined after the array formula has been
|
|
33843
|
+
// evaluated. In the case where the impacted cells are used in other formulas,
|
|
33844
|
+
// this will require to re-evaluation other formulas (and so on...). ex:
|
|
33845
|
+
// A1:"=B2"
|
|
33846
|
+
// A2:"=SPLIT('Odoo','d')"
|
|
33847
|
+
// in the example above, A2 spreads on B2, but we will know it only after
|
|
33848
|
+
// the evaluation of A2. To be able to evaluate A1 correctly, we must therefore
|
|
33849
|
+
// reevaluate A1 after the evaluation of A2.
|
|
33850
|
+
// We could evaluate which formula spreads first. Except that the array formulas
|
|
33851
|
+
// can themselves depend on the spreads of other formulas. ex:
|
|
33852
|
+
// A1:"=SPLIT(B3,'d')"
|
|
33853
|
+
// A2:="odoo odoo"
|
|
33854
|
+
// A3:"=SPLIT(A2,' ')"
|
|
33855
|
+
// In the example above, A3 must be evaluated before A1 because A1 needs B3 which
|
|
33856
|
+
// can be modified by A3.
|
|
33857
|
+
// Therefore, there would be a spatial evaluation order to be respected between
|
|
33858
|
+
// the array formulas. We could imagine that, when an array formula depends
|
|
33859
|
+
// on a cell, then we evaluate the first formula that spreads located in the upper
|
|
33860
|
+
// left corner of this cell.
|
|
33861
|
+
// Although this possibility has been explored, it remains complicated to spatially
|
|
33862
|
+
// predict which formula should be evaluated before another, especially when
|
|
33863
|
+
// the array formulas are located in different sheets or when the array formulas
|
|
33864
|
+
// depends on the spreads of each other. ex:
|
|
33865
|
+
//
|
|
33866
|
+
// A1:"=ARRAY_FORMULA_ALPHA(B2)"
|
|
33867
|
+
// A2:"=ARRAY_FORMULA_BETA(B1)"
|
|
33868
|
+
// In the example above, ARRAY_FORMULA_ALPHA and ARRAY_FORMULA_BETA are some
|
|
33869
|
+
// formulas that could spread depending on the value of B2 and B1. This could be a
|
|
33870
|
+
// cyclic dependency that we cannot anticipate.
|
|
33871
|
+
// And as with the "IF" formula, array formulas may not use their dependency.
|
|
33872
|
+
// It then becomes very difficult to manage...
|
|
33873
|
+
// Also, if we have a cycle, that doesn't mean it's bad. The cycle can converge to
|
|
33874
|
+
// a stable state at the scale of the sheets. Functionally, we don't want to forbid
|
|
33875
|
+
// convergent cycles. It is an interesting feature but which requires to re-evaluate
|
|
33876
|
+
// the cycle as many times as convergence is not reached.
|
|
33877
|
+
// Thus, in order to respect the relations between the cells (formula dependencies and
|
|
33878
|
+
// spread relations), the evaluation of the cells must:
|
|
33879
|
+
// - respect a precise order (cells values used by another must be evaluated first) : As
|
|
33880
|
+
// we cannot anticipate which dependencies are really used by the formulas, we must
|
|
33881
|
+
// evaluate the cells in a recursive way;
|
|
33882
|
+
// - be done as many times as necessary to ensure that all the cells have been correctly
|
|
33883
|
+
// evaluated in the correct order (in case of, for example, spreading relation cycles).
|
|
33884
|
+
// The chosen solution is to reevaluate the formulas impacted by spreads as many times
|
|
33885
|
+
// as necessary in several iterations, where evaluated cells can trigger the evaluation
|
|
33886
|
+
// of other cells depending on it, at the next iteration.
|
|
33887
|
+
//#endregion
|
|
33888
|
+
class EvaluationPlugin extends UIPlugin {
|
|
33889
|
+
static getters = [
|
|
33890
|
+
"evaluateFormula",
|
|
33891
|
+
"getRangeFormattedValues",
|
|
33892
|
+
"getRangeValues",
|
|
33893
|
+
"getRangeFormats",
|
|
33894
|
+
"getEvaluatedCell",
|
|
33895
|
+
"getEvaluatedCells",
|
|
33896
|
+
"getEvaluatedCellsInZone",
|
|
33897
|
+
];
|
|
33898
|
+
shouldRebuildDependenciesGraph = true;
|
|
33899
|
+
evaluator;
|
|
33900
|
+
compilationParams;
|
|
33901
|
+
positionsToUpdate = [];
|
|
33902
|
+
constructor(config) {
|
|
33903
|
+
super(config);
|
|
33904
|
+
this.evaluator = new Evaluator(config.custom, this.getters);
|
|
33905
|
+
this.compilationParams = buildCompilationParameters(config.custom, this.getters, (position) => this.evaluator.getEvaluatedCell(position));
|
|
33906
|
+
}
|
|
33907
|
+
// ---------------------------------------------------------------------------
|
|
33908
|
+
// Command Handling
|
|
33909
|
+
// ---------------------------------------------------------------------------
|
|
33910
|
+
beforeHandle(cmd) {
|
|
33911
|
+
if (invalidateDependenciesCommands.has(cmd.type)) {
|
|
33912
|
+
this.shouldRebuildDependenciesGraph = true;
|
|
33913
|
+
}
|
|
33914
|
+
}
|
|
33915
|
+
handle(cmd) {
|
|
33916
|
+
switch (cmd.type) {
|
|
33917
|
+
case "UPDATE_CELL":
|
|
33918
|
+
if (!("content" in cmd || "format" in cmd) || this.shouldRebuildDependenciesGraph) {
|
|
33919
|
+
return;
|
|
33920
|
+
}
|
|
33921
|
+
this.positionsToUpdate.push(cmd);
|
|
33922
|
+
if ("content" in cmd) {
|
|
33923
|
+
this.evaluator.updateDependencies(cmd);
|
|
33924
|
+
}
|
|
33925
|
+
break;
|
|
33926
|
+
case "EVALUATE_CELLS":
|
|
33927
|
+
this.evaluator.evaluateAllCells();
|
|
33928
|
+
break;
|
|
33929
|
+
}
|
|
33930
|
+
}
|
|
33931
|
+
finalize() {
|
|
33932
|
+
if (this.shouldRebuildDependenciesGraph) {
|
|
33933
|
+
this.evaluator.buildDependencyGraph();
|
|
33934
|
+
this.evaluator.evaluateAllCells();
|
|
33935
|
+
this.shouldRebuildDependenciesGraph = false;
|
|
33936
|
+
}
|
|
33937
|
+
else if (this.positionsToUpdate.length) {
|
|
33938
|
+
this.evaluator.evaluateCells(this.positionsToUpdate);
|
|
33939
|
+
}
|
|
33940
|
+
this.positionsToUpdate = [];
|
|
33941
|
+
}
|
|
33942
|
+
// ---------------------------------------------------------------------------
|
|
33943
|
+
// Getters
|
|
33944
|
+
// ---------------------------------------------------------------------------
|
|
33945
|
+
evaluateFormula(formulaString, sheetId = this.getters.getActiveSheetId()) {
|
|
33946
|
+
const compiledFormula = compile(formulaString);
|
|
33947
|
+
const ranges = [];
|
|
33948
|
+
for (let xc of compiledFormula.dependencies) {
|
|
33949
|
+
ranges.push(this.getters.getRangeFromSheetXC(sheetId, xc));
|
|
33950
|
+
}
|
|
33951
|
+
return compiledFormula.execute(ranges, ...this.compilationParams).value;
|
|
33952
|
+
}
|
|
33953
|
+
/**
|
|
33954
|
+
* Return the value of each cell in the range as they are displayed in the grid.
|
|
33955
|
+
*/
|
|
33956
|
+
getRangeFormattedValues(range) {
|
|
33957
|
+
const sheet = this.getters.tryGetSheet(range.sheetId);
|
|
33958
|
+
if (sheet === undefined)
|
|
33959
|
+
return [];
|
|
33960
|
+
return this.getters
|
|
33961
|
+
.getEvaluatedCellsInZone(sheet.id, range.zone)
|
|
33962
|
+
.map((cell) => cell.formattedValue);
|
|
33963
|
+
}
|
|
33964
|
+
/**
|
|
33965
|
+
* Return the value of each cell in the range.
|
|
33966
|
+
*/
|
|
33967
|
+
getRangeValues(range) {
|
|
33968
|
+
const sheet = this.getters.tryGetSheet(range.sheetId);
|
|
33969
|
+
if (sheet === undefined)
|
|
33970
|
+
return [];
|
|
33971
|
+
return this.getters.getEvaluatedCellsInZone(sheet.id, range.zone).map((cell) => cell.value);
|
|
33972
|
+
}
|
|
33973
|
+
/**
|
|
33974
|
+
* Return the format of each cell in the range.
|
|
33975
|
+
*/
|
|
33976
|
+
getRangeFormats(range) {
|
|
33977
|
+
const sheet = this.getters.tryGetSheet(range.sheetId);
|
|
33978
|
+
if (sheet === undefined)
|
|
33979
|
+
return [];
|
|
33980
|
+
return this.getters.getEvaluatedCellsInZone(sheet.id, range.zone).map((cell) => cell.format);
|
|
33981
|
+
}
|
|
33982
|
+
getEvaluatedCell(position) {
|
|
33983
|
+
return this.evaluator.getEvaluatedCell(position);
|
|
33984
|
+
}
|
|
33985
|
+
getEvaluatedCells(sheetId) {
|
|
33986
|
+
const rawCells = this.getters.getCells(sheetId) || {};
|
|
33987
|
+
const record = {};
|
|
33988
|
+
for (let cellId of Object.keys(rawCells)) {
|
|
33989
|
+
const position = this.getters.getCellPosition(cellId);
|
|
33990
|
+
record[cellId] = this.getEvaluatedCell(position);
|
|
33991
|
+
}
|
|
33992
|
+
return record;
|
|
33993
|
+
}
|
|
33994
|
+
getEvaluatedCellsInZone(sheetId, zone) {
|
|
33995
|
+
return positions(zone).map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }));
|
|
33996
|
+
}
|
|
33997
|
+
// ---------------------------------------------------------------------------
|
|
33998
|
+
// Export
|
|
33999
|
+
// ---------------------------------------------------------------------------
|
|
34000
|
+
exportForExcel(data) {
|
|
34001
|
+
for (const position of this.evaluator.getEvaluatedPositions()) {
|
|
34002
|
+
const evaluatedCell = this.evaluator.getEvaluatedCell(position);
|
|
34003
|
+
const xc = toXC(position.col, position.row);
|
|
34004
|
+
const value = evaluatedCell.value;
|
|
34005
|
+
let isFormula = false;
|
|
34006
|
+
let newContent = undefined;
|
|
34007
|
+
let newFormat = undefined;
|
|
34008
|
+
let isExported = true;
|
|
34009
|
+
const formulaCell = this.getCorrespondingFormulaCell(position);
|
|
34010
|
+
if (formulaCell) {
|
|
34011
|
+
isExported = formulaCell.compiledFormula.tokens
|
|
34012
|
+
.filter((tk) => tk.type === "FUNCTION")
|
|
34013
|
+
.every((tk) => functions[tk.value.toUpperCase()].isExported);
|
|
34014
|
+
isFormula = isExported;
|
|
34015
|
+
if (!isExported) {
|
|
34016
|
+
newContent = value.toString();
|
|
34017
|
+
newFormat = evaluatedCell.format;
|
|
34018
|
+
}
|
|
34019
|
+
}
|
|
34020
|
+
const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
|
|
34021
|
+
const exportedCellData = exportedSheetData.cells[xc] || {};
|
|
34022
|
+
const format = newFormat
|
|
34023
|
+
? getItemId(newFormat, data.formats)
|
|
34024
|
+
: exportedCellData.format;
|
|
34025
|
+
const content = !isExported ? newContent : exportedCellData.content;
|
|
34026
|
+
exportedSheetData.cells[xc] = { ...exportedCellData, value, isFormula, content, format };
|
|
34027
|
+
}
|
|
34028
|
+
}
|
|
34029
|
+
/**
|
|
34030
|
+
* Returns the corresponding formula cell of a given cell
|
|
34031
|
+
* It could be the formula present in the cell itself or the
|
|
34032
|
+
* formula of the array formula that spreads to the cell
|
|
34033
|
+
*/
|
|
34034
|
+
getCorrespondingFormulaCell(position) {
|
|
34035
|
+
const cell = this.getters.getCell(position);
|
|
34036
|
+
if (cell && cell.content) {
|
|
34037
|
+
if (cell.isFormula && !isBadExpression(cell.content)) {
|
|
34038
|
+
return cell;
|
|
34039
|
+
}
|
|
34040
|
+
return undefined;
|
|
34041
|
+
}
|
|
34042
|
+
const spreadingFormulaPosition = this.evaluator.getArrayFormulaSpreadingOn(position);
|
|
34043
|
+
if (spreadingFormulaPosition === undefined) {
|
|
34044
|
+
return undefined;
|
|
34045
|
+
}
|
|
34046
|
+
const spreadingFormulaCell = this.getters.getCell(spreadingFormulaPosition);
|
|
34047
|
+
if (spreadingFormulaCell?.isFormula) {
|
|
34048
|
+
return spreadingFormulaCell;
|
|
34049
|
+
}
|
|
34050
|
+
return undefined;
|
|
34051
|
+
}
|
|
34052
|
+
}
|
|
34053
|
+
function isBadExpression(formula) {
|
|
34054
|
+
try {
|
|
34055
|
+
compile(formula);
|
|
34056
|
+
return false;
|
|
34057
|
+
}
|
|
34058
|
+
catch (error) {
|
|
34059
|
+
return true;
|
|
34060
|
+
}
|
|
34061
|
+
}
|
|
34062
|
+
|
|
33085
34063
|
/**
|
|
33086
34064
|
* https://tomekdev.com/posts/sorting-colors-in-js
|
|
33087
34065
|
*/
|
|
@@ -33232,329 +34210,6 @@ class CustomColorsPlugin extends UIPlugin {
|
|
|
33232
34210
|
}
|
|
33233
34211
|
}
|
|
33234
34212
|
|
|
33235
|
-
const functionMap = functionRegistry.mapping;
|
|
33236
|
-
class EvaluationPlugin extends UIPlugin {
|
|
33237
|
-
static getters = [
|
|
33238
|
-
"evaluateFormula",
|
|
33239
|
-
"getRangeFormattedValues",
|
|
33240
|
-
"getRangeValues",
|
|
33241
|
-
"getRangeFormats",
|
|
33242
|
-
"getEvaluatedCell",
|
|
33243
|
-
"getEvaluatedCells",
|
|
33244
|
-
"getColEvaluatedCells",
|
|
33245
|
-
"getEvaluatedCellsInZone",
|
|
33246
|
-
];
|
|
33247
|
-
isUpToDate = false;
|
|
33248
|
-
evaluatedCells = {};
|
|
33249
|
-
evalContext;
|
|
33250
|
-
lazyEvaluation;
|
|
33251
|
-
constructor(config) {
|
|
33252
|
-
super(config);
|
|
33253
|
-
this.evalContext = config.custom;
|
|
33254
|
-
this.lazyEvaluation = config.lazyEvaluation;
|
|
33255
|
-
}
|
|
33256
|
-
// ---------------------------------------------------------------------------
|
|
33257
|
-
// Command Handling
|
|
33258
|
-
// ---------------------------------------------------------------------------
|
|
33259
|
-
handle(cmd) {
|
|
33260
|
-
if (invalidateEvaluationCommands.has(cmd.type)) {
|
|
33261
|
-
this.isUpToDate = false;
|
|
33262
|
-
}
|
|
33263
|
-
switch (cmd.type) {
|
|
33264
|
-
case "UPDATE_CELL":
|
|
33265
|
-
if ("content" in cmd || "format" in cmd) {
|
|
33266
|
-
this.isUpToDate = false;
|
|
33267
|
-
}
|
|
33268
|
-
break;
|
|
33269
|
-
case "EVALUATE_CELLS":
|
|
33270
|
-
this.evaluate();
|
|
33271
|
-
break;
|
|
33272
|
-
}
|
|
33273
|
-
}
|
|
33274
|
-
finalize() {
|
|
33275
|
-
if (!this.isUpToDate) {
|
|
33276
|
-
this.evaluate();
|
|
33277
|
-
this.isUpToDate = true;
|
|
33278
|
-
}
|
|
33279
|
-
}
|
|
33280
|
-
// ---------------------------------------------------------------------------
|
|
33281
|
-
// Getters
|
|
33282
|
-
// ---------------------------------------------------------------------------
|
|
33283
|
-
evaluateFormula(formulaString, sheetId = this.getters.getActiveSheetId()) {
|
|
33284
|
-
const compiledFormula = compile(formulaString);
|
|
33285
|
-
const params = this.getCompilationParameters((cell) => this.getEvaluatedCell(this.getters.getCellPosition(cell.id)));
|
|
33286
|
-
const ranges = [];
|
|
33287
|
-
for (let xc of compiledFormula.dependencies) {
|
|
33288
|
-
ranges.push(this.getters.getRangeFromSheetXC(sheetId, xc));
|
|
33289
|
-
}
|
|
33290
|
-
return compiledFormula.execute(ranges, ...params).value;
|
|
33291
|
-
}
|
|
33292
|
-
/**
|
|
33293
|
-
* Return the value of each cell in the range as they are displayed in the grid.
|
|
33294
|
-
*/
|
|
33295
|
-
getRangeFormattedValues(range) {
|
|
33296
|
-
const sheet = this.getters.tryGetSheet(range.sheetId);
|
|
33297
|
-
if (sheet === undefined)
|
|
33298
|
-
return [];
|
|
33299
|
-
return this.getters
|
|
33300
|
-
.getEvaluatedCellsInZone(sheet.id, range.zone)
|
|
33301
|
-
.map((cell) => cell.formattedValue);
|
|
33302
|
-
}
|
|
33303
|
-
/**
|
|
33304
|
-
* Return the value of each cell in the range.
|
|
33305
|
-
*/
|
|
33306
|
-
getRangeValues(range) {
|
|
33307
|
-
const sheet = this.getters.tryGetSheet(range.sheetId);
|
|
33308
|
-
if (sheet === undefined)
|
|
33309
|
-
return [];
|
|
33310
|
-
return this.getters.getEvaluatedCellsInZone(sheet.id, range.zone).map((cell) => cell.value);
|
|
33311
|
-
}
|
|
33312
|
-
/**
|
|
33313
|
-
* Return the format of each cell in the range.
|
|
33314
|
-
*/
|
|
33315
|
-
getRangeFormats(range) {
|
|
33316
|
-
const sheet = this.getters.tryGetSheet(range.sheetId);
|
|
33317
|
-
if (sheet === undefined)
|
|
33318
|
-
return [];
|
|
33319
|
-
return this.getters.getEvaluatedCellsInZone(sheet.id, range.zone).map((cell) => cell.format);
|
|
33320
|
-
}
|
|
33321
|
-
getEvaluatedCell({ sheetId, col, row }) {
|
|
33322
|
-
const cell = this.getters.getCell({ sheetId, col, row });
|
|
33323
|
-
if (cell === undefined) {
|
|
33324
|
-
return createEvaluatedCell("");
|
|
33325
|
-
}
|
|
33326
|
-
// the cell might have been created by a command in the current
|
|
33327
|
-
// dispatch but the evaluation is not done yet.
|
|
33328
|
-
return this.evaluatedCells[sheetId]?.[col]?.[row]?.() || createEvaluatedCell("");
|
|
33329
|
-
}
|
|
33330
|
-
getEvaluatedCells(sheetId) {
|
|
33331
|
-
const rawCells = this.getters.getCells(sheetId) || {};
|
|
33332
|
-
const record = {};
|
|
33333
|
-
for (let cellId of Object.keys(rawCells)) {
|
|
33334
|
-
const position = this.getters.getCellPosition(cellId);
|
|
33335
|
-
record[cellId] = this.getEvaluatedCell(position);
|
|
33336
|
-
}
|
|
33337
|
-
return record;
|
|
33338
|
-
}
|
|
33339
|
-
/**
|
|
33340
|
-
* Returns all the evaluated cells of a col
|
|
33341
|
-
*/
|
|
33342
|
-
getColEvaluatedCells(sheetId, col) {
|
|
33343
|
-
return Object.values(this.evaluatedCells[sheetId]?.[col] || [])
|
|
33344
|
-
.filter(isDefined$1)
|
|
33345
|
-
.map((lazyCell) => lazyCell());
|
|
33346
|
-
}
|
|
33347
|
-
getEvaluatedCellsInZone(sheetId, zone) {
|
|
33348
|
-
return positions(zone).map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }));
|
|
33349
|
-
}
|
|
33350
|
-
// ---------------------------------------------------------------------------
|
|
33351
|
-
// Evaluator
|
|
33352
|
-
// ---------------------------------------------------------------------------
|
|
33353
|
-
setEvaluatedCell(cellId, evaluatedCell) {
|
|
33354
|
-
const { col, row, sheetId } = this.getters.getCellPosition(cellId);
|
|
33355
|
-
if (!this.evaluatedCells[sheetId]) {
|
|
33356
|
-
this.evaluatedCells[sheetId] = {};
|
|
33357
|
-
}
|
|
33358
|
-
if (!this.evaluatedCells[sheetId][col]) {
|
|
33359
|
-
this.evaluatedCells[sheetId][col] = {};
|
|
33360
|
-
}
|
|
33361
|
-
this.evaluatedCells[sheetId][col][row] = evaluatedCell;
|
|
33362
|
-
if (!this.lazyEvaluation) {
|
|
33363
|
-
this.evaluatedCells[sheetId][col][row]();
|
|
33364
|
-
}
|
|
33365
|
-
}
|
|
33366
|
-
*getAllCells() {
|
|
33367
|
-
// use a generator function to avoid re-building a new object
|
|
33368
|
-
for (const sheetId of this.getters.getSheetIds()) {
|
|
33369
|
-
const cells = this.getters.getCells(sheetId);
|
|
33370
|
-
for (const cellId in cells) {
|
|
33371
|
-
yield cells[cellId];
|
|
33372
|
-
}
|
|
33373
|
-
}
|
|
33374
|
-
}
|
|
33375
|
-
evaluate() {
|
|
33376
|
-
this.evaluatedCells = {};
|
|
33377
|
-
const cellsBeingComputed = new Set();
|
|
33378
|
-
const computeCell = (cell) => {
|
|
33379
|
-
const cellId = cell.id;
|
|
33380
|
-
const { col, row, sheetId } = this.getters.getCellPosition(cellId);
|
|
33381
|
-
const lazyEvaluation = this.evaluatedCells[sheetId]?.[col]?.[row];
|
|
33382
|
-
if (lazyEvaluation) {
|
|
33383
|
-
return lazyEvaluation; // already computed
|
|
33384
|
-
}
|
|
33385
|
-
return lazy(() => {
|
|
33386
|
-
try {
|
|
33387
|
-
switch (cell.isFormula) {
|
|
33388
|
-
case true:
|
|
33389
|
-
return computeFormulaCell(cell);
|
|
33390
|
-
case false:
|
|
33391
|
-
return evaluateLiteral(cell.content, cell.format);
|
|
33392
|
-
}
|
|
33393
|
-
}
|
|
33394
|
-
catch (e) {
|
|
33395
|
-
return handleError(e, cell);
|
|
33396
|
-
}
|
|
33397
|
-
});
|
|
33398
|
-
};
|
|
33399
|
-
const handleError = (e, cell) => {
|
|
33400
|
-
if (!(e instanceof Error)) {
|
|
33401
|
-
e = new Error(e);
|
|
33402
|
-
}
|
|
33403
|
-
const msg = e?.errorType || CellErrorType.GenericError;
|
|
33404
|
-
// apply function name
|
|
33405
|
-
const __lastFnCalled = compilationParameters[2].__lastFnCalled || "";
|
|
33406
|
-
const error = new EvaluationError(msg, e.message.replace("[[FUNCTION_NAME]]", __lastFnCalled), e.logLevel !== undefined ? e.logLevel : CellErrorLevel.error);
|
|
33407
|
-
return errorCell(cell.content, error);
|
|
33408
|
-
};
|
|
33409
|
-
const computeFormulaCell = (cellData) => {
|
|
33410
|
-
const cellId = cellData.id;
|
|
33411
|
-
if (cellsBeingComputed.has(cellId)) {
|
|
33412
|
-
throw new CircularDependencyError();
|
|
33413
|
-
}
|
|
33414
|
-
compilationParameters[2].__originCellXC = () => {
|
|
33415
|
-
// compute the value lazily for performance reasons
|
|
33416
|
-
const position = compilationParameters[2].getters.getCellPosition(cellId);
|
|
33417
|
-
return toXC(position.col, position.row);
|
|
33418
|
-
};
|
|
33419
|
-
cellsBeingComputed.add(cellId);
|
|
33420
|
-
const computedCell = cellData.compiledFormula.execute(cellData.dependencies, ...compilationParameters);
|
|
33421
|
-
cellsBeingComputed.delete(cellId);
|
|
33422
|
-
if (Array.isArray(computedCell.value)) {
|
|
33423
|
-
// if a value returns an array (like =A1:A3)
|
|
33424
|
-
throw new Error(_lt("This formula depends on invalid values"));
|
|
33425
|
-
}
|
|
33426
|
-
return createEvaluatedCell(computedCell.value, cellData.format || computedCell.format);
|
|
33427
|
-
};
|
|
33428
|
-
const compilationParameters = this.getCompilationParameters((cell) => computeCell(cell)());
|
|
33429
|
-
for (const cell of this.getAllCells()) {
|
|
33430
|
-
this.setEvaluatedCell(cell.id, computeCell(cell));
|
|
33431
|
-
}
|
|
33432
|
-
}
|
|
33433
|
-
/**
|
|
33434
|
-
* Return all functions necessary to properly evaluate a formula:
|
|
33435
|
-
* - a refFn function to read any reference, cell or range of a normalized formula
|
|
33436
|
-
* - a range function to convert any reference to a proper value array
|
|
33437
|
-
* - an evaluation context
|
|
33438
|
-
*/
|
|
33439
|
-
getCompilationParameters(computeCell) {
|
|
33440
|
-
const evalContext = Object.assign(Object.create(functionMap), this.evalContext, {
|
|
33441
|
-
getters: this.getters,
|
|
33442
|
-
});
|
|
33443
|
-
const getters = this.getters;
|
|
33444
|
-
function readCell(range) {
|
|
33445
|
-
let cell;
|
|
33446
|
-
if (!getters.tryGetSheet(range.sheetId)) {
|
|
33447
|
-
throw new Error(_lt("Invalid sheet name"));
|
|
33448
|
-
}
|
|
33449
|
-
cell = getters.getCell({ sheetId: range.sheetId, col: range.zone.left, row: range.zone.top });
|
|
33450
|
-
if (!cell || cell.content === "") {
|
|
33451
|
-
// magic "empty" value
|
|
33452
|
-
// Returning {value: null} instead of undefined will ensure that we don't
|
|
33453
|
-
// fall back on the default value of the argument provided to the formula's compute function
|
|
33454
|
-
return { value: null, format: cell?.format };
|
|
33455
|
-
}
|
|
33456
|
-
return getEvaluatedCell(cell);
|
|
33457
|
-
}
|
|
33458
|
-
const getEvaluatedCell = (cell) => {
|
|
33459
|
-
const evaluatedCell = computeCell(cell);
|
|
33460
|
-
if (evaluatedCell.type === CellValueType.error) {
|
|
33461
|
-
throw evaluatedCell.error;
|
|
33462
|
-
}
|
|
33463
|
-
return evaluatedCell;
|
|
33464
|
-
};
|
|
33465
|
-
/**
|
|
33466
|
-
* Return the values of the cell(s) used in reference, but always in the format of a range even
|
|
33467
|
-
* if a single cell is referenced. It is a list of col values. This is useful for the formulas that describe parameters as
|
|
33468
|
-
* range<number> etc.
|
|
33469
|
-
*
|
|
33470
|
-
* Note that each col is possibly sparse: it only contain the values of cells
|
|
33471
|
-
* that are actually present in the grid.
|
|
33472
|
-
*/
|
|
33473
|
-
function range(range) {
|
|
33474
|
-
const sheetId = range.sheetId;
|
|
33475
|
-
if (!isZoneValid(range.zone)) {
|
|
33476
|
-
throw new InvalidReferenceError();
|
|
33477
|
-
}
|
|
33478
|
-
// Performance issue: Avoid fetching data on positions that are out of the spreadsheet
|
|
33479
|
-
// e.g. A1:ZZZ9999 in a sheet with 10 cols and 10 rows should ignore everything past J10 and return a 10x10 array
|
|
33480
|
-
const sheetZone = getters.getSheetZone(sheetId);
|
|
33481
|
-
const result = [];
|
|
33482
|
-
const zone = intersection(range.zone, sheetZone);
|
|
33483
|
-
if (!zone) {
|
|
33484
|
-
result.push([]);
|
|
33485
|
-
return result;
|
|
33486
|
-
}
|
|
33487
|
-
// Performance issue: nested loop is faster than a map here
|
|
33488
|
-
for (let col = zone.left; col <= zone.right; col++) {
|
|
33489
|
-
const rowValues = [];
|
|
33490
|
-
for (let row = zone.top; row <= zone.bottom; row++) {
|
|
33491
|
-
const cell = evalContext.getters.getCell({ sheetId: range.sheetId, col, row });
|
|
33492
|
-
rowValues.push(cell ? getEvaluatedCell(cell) : undefined);
|
|
33493
|
-
}
|
|
33494
|
-
result.push(rowValues);
|
|
33495
|
-
}
|
|
33496
|
-
return result;
|
|
33497
|
-
}
|
|
33498
|
-
/**
|
|
33499
|
-
* Returns the value of the cell(s) used in reference
|
|
33500
|
-
*
|
|
33501
|
-
* @param range the references used
|
|
33502
|
-
* @param isMeta if a reference is supposed to be used in a `meta` parameter as described in the
|
|
33503
|
-
* function for which this parameter is used, we just return the string of the parameter.
|
|
33504
|
-
* The `compute` of the formula's function must process it completely
|
|
33505
|
-
*/
|
|
33506
|
-
function refFn(range, isMeta, functionName, paramNumber) {
|
|
33507
|
-
if (isMeta) {
|
|
33508
|
-
// Use zoneToXc of zone instead of getRangeString to avoid sending unbounded ranges
|
|
33509
|
-
return { value: zoneToXc(range.zone) };
|
|
33510
|
-
}
|
|
33511
|
-
if (!isZoneValid(range.zone)) {
|
|
33512
|
-
throw new InvalidReferenceError();
|
|
33513
|
-
}
|
|
33514
|
-
// if the formula definition could have accepted a range, we would pass through the _range function and not here
|
|
33515
|
-
if (range.zone.bottom !== range.zone.top || range.zone.left !== range.zone.right) {
|
|
33516
|
-
throw new Error(paramNumber
|
|
33517
|
-
? _lt("Function %s expects the parameter %s to be a single value or a single cell reference, not a range.", functionName.toString(), paramNumber.toString())
|
|
33518
|
-
: _lt("Function %s expects its parameters to be single values or single cell references, not ranges.", functionName.toString()));
|
|
33519
|
-
}
|
|
33520
|
-
if (range.invalidSheetName) {
|
|
33521
|
-
throw new Error(_lt("Invalid sheet name: %s", range.invalidSheetName));
|
|
33522
|
-
}
|
|
33523
|
-
return readCell(range);
|
|
33524
|
-
}
|
|
33525
|
-
return [refFn, range, evalContext];
|
|
33526
|
-
}
|
|
33527
|
-
// ---------------------------------------------------------------------------
|
|
33528
|
-
// Export
|
|
33529
|
-
// ---------------------------------------------------------------------------
|
|
33530
|
-
exportForExcel(data) {
|
|
33531
|
-
for (let sheet of data.sheets) {
|
|
33532
|
-
for (const xc in sheet.cells) {
|
|
33533
|
-
const position = { sheetId: sheet.id, ...toCartesian(xc) };
|
|
33534
|
-
const cell = this.getters.getCell(position);
|
|
33535
|
-
if (cell) {
|
|
33536
|
-
const exportedCellData = sheet.cells[xc];
|
|
33537
|
-
const evaluatedCell = this.getEvaluatedCell(position);
|
|
33538
|
-
exportedCellData.value = evaluatedCell.value;
|
|
33539
|
-
exportedCellData.isFormula = cell.isFormula && !this.isBadExpression(cell.content);
|
|
33540
|
-
if (cell.format !== evaluatedCell.format) {
|
|
33541
|
-
exportedCellData.computedFormat = evaluatedCell.format;
|
|
33542
|
-
}
|
|
33543
|
-
}
|
|
33544
|
-
}
|
|
33545
|
-
}
|
|
33546
|
-
}
|
|
33547
|
-
isBadExpression(formula) {
|
|
33548
|
-
try {
|
|
33549
|
-
compile(formula);
|
|
33550
|
-
return false;
|
|
33551
|
-
}
|
|
33552
|
-
catch (error) {
|
|
33553
|
-
return true;
|
|
33554
|
-
}
|
|
33555
|
-
}
|
|
33556
|
-
}
|
|
33557
|
-
|
|
33558
34213
|
class EvaluationChartPlugin extends UIPlugin {
|
|
33559
34214
|
static getters = ["getChartRuntime", "getBackgroundOfSingleCellChart"];
|
|
33560
34215
|
charts = {};
|
|
@@ -38668,7 +39323,8 @@ class SheetUIPlugin extends UIPlugin {
|
|
|
38668
39323
|
getCellWidth(position) {
|
|
38669
39324
|
const text = this.getCellText(position);
|
|
38670
39325
|
const style = this.getters.getCellComputedStyle(position);
|
|
38671
|
-
|
|
39326
|
+
const multiLineText = text.split(NEWLINE);
|
|
39327
|
+
let contentWidth = Math.max(...multiLineText.map((line) => this.getTextWidth(line, style)));
|
|
38672
39328
|
const icon = this.getters.getConditionalIcon(position);
|
|
38673
39329
|
if (icon) {
|
|
38674
39330
|
contentWidth += computeIconWidth(this.getters.getCellStyle(position));
|
|
@@ -43797,6 +44453,10 @@ class Spreadsheet extends owl.Component {
|
|
|
43797
44453
|
}
|
|
43798
44454
|
checkViewportSize() {
|
|
43799
44455
|
const { xRatio, yRatio } = this.env.model.getters.getFrozenSheetViewRatio(this.env.model.getters.getActiveSheetId());
|
|
44456
|
+
if (!isFinite(xRatio) || !isFinite(yRatio)) {
|
|
44457
|
+
// before mounting, the ratios can be NaN or Infinity if the viewport size is 0
|
|
44458
|
+
return;
|
|
44459
|
+
}
|
|
43800
44460
|
if (yRatio > MAXIMAL_FREEZABLE_RATIO || xRatio > MAXIMAL_FREEZABLE_RATIO) {
|
|
43801
44461
|
if (this.isViewportTooSmall) {
|
|
43802
44462
|
return;
|
|
@@ -45768,44 +46428,30 @@ function numberRef(reference) {
|
|
|
45768
46428
|
|
|
45769
46429
|
function addFormula(cell) {
|
|
45770
46430
|
const formula = cell.content;
|
|
45771
|
-
const functions = functionRegistry.content;
|
|
45772
|
-
const tokens = tokenize(formula);
|
|
45773
46431
|
const attrs = [];
|
|
45774
46432
|
let node = escapeXml ``;
|
|
45775
|
-
|
|
45776
|
-
|
|
45777
|
-
|
|
45778
|
-
|
|
45779
|
-
|
|
45780
|
-
|
|
45781
|
-
|
|
45782
|
-
|
|
45783
|
-
|
|
45784
|
-
attrs.push(["t", "str"]);
|
|
45785
|
-
cycle = escapeXml /*xml*/ `<v>${cell.value}</v>`;
|
|
45786
|
-
}
|
|
45787
|
-
node = escapeXml /*xml*/ `
|
|
46433
|
+
let cycle = escapeXml ``;
|
|
46434
|
+
const XlsxFormula = adaptFormulaToExcel(formula);
|
|
46435
|
+
// hack for cycles : if we don't set a value (be it 0 or #VALUE!), it will appear as invisible on excel,
|
|
46436
|
+
// Making it very hard for the client to find where the recursion is.
|
|
46437
|
+
if (cell.value === CellErrorType.CircularDependency) {
|
|
46438
|
+
attrs.push(["t", "str"]);
|
|
46439
|
+
cycle = escapeXml /*xml*/ `<v>${cell.value}</v>`;
|
|
46440
|
+
}
|
|
46441
|
+
node = escapeXml /*xml*/ `
|
|
45788
46442
|
<f>
|
|
45789
46443
|
${XlsxFormula}
|
|
45790
46444
|
</f>
|
|
45791
46445
|
${cycle}
|
|
45792
46446
|
`;
|
|
45793
|
-
|
|
45794
|
-
}
|
|
45795
|
-
else {
|
|
45796
|
-
// Shouldn't we always output the value then ?
|
|
45797
|
-
const value = cell.value;
|
|
45798
|
-
const type = getCellType(value);
|
|
45799
|
-
attrs.push(["t", type]);
|
|
45800
|
-
node = escapeXml /*xml*/ `<v>${value}</v>`;
|
|
45801
|
-
return { attrs, node };
|
|
45802
|
-
}
|
|
46447
|
+
return { attrs, node };
|
|
45803
46448
|
}
|
|
45804
46449
|
function addContent(content, sharedStrings, forceString = false) {
|
|
45805
46450
|
let value = content;
|
|
45806
46451
|
const attrs = [];
|
|
45807
|
-
|
|
45808
|
-
|
|
46452
|
+
const clearValue = value.trim().toUpperCase();
|
|
46453
|
+
if (!forceString && ["TRUE", "FALSE"].includes(clearValue)) {
|
|
46454
|
+
value = clearValue === "TRUE" ? "1" : "0";
|
|
45809
46455
|
attrs.push(["t", "b"]);
|
|
45810
46456
|
}
|
|
45811
46457
|
else if (forceString || !isNumber(value)) {
|
|
@@ -47196,7 +47842,6 @@ class Model extends EventBus {
|
|
|
47196
47842
|
moveClient: () => { },
|
|
47197
47843
|
snapshotRequested: false,
|
|
47198
47844
|
notifyUI: (payload) => this.trigger("notify-ui", payload),
|
|
47199
|
-
lazyEvaluation: "lazyEvaluation" in config ? config.lazyEvaluation : true,
|
|
47200
47845
|
};
|
|
47201
47846
|
}
|
|
47202
47847
|
setupCorePluginConfig() {
|
|
@@ -47219,7 +47864,6 @@ class Model extends EventBus {
|
|
|
47219
47864
|
moveClient: this.session.move.bind(this.session),
|
|
47220
47865
|
custom: this.config.custom,
|
|
47221
47866
|
uiActions: this.config,
|
|
47222
|
-
lazyEvaluation: this.config.lazyEvaluation,
|
|
47223
47867
|
session: this.session,
|
|
47224
47868
|
};
|
|
47225
47869
|
}
|
|
@@ -47558,6 +48202,6 @@ exports.setTranslationMethod = setTranslationMethod;
|
|
|
47558
48202
|
exports.tokenize = tokenize;
|
|
47559
48203
|
|
|
47560
48204
|
|
|
47561
|
-
__info__.version = '16.4.0-alpha.
|
|
47562
|
-
__info__.date = '2023-05-
|
|
47563
|
-
__info__.hash = '
|
|
48205
|
+
__info__.version = '16.4.0-alpha.1';
|
|
48206
|
+
__info__.date = '2023-05-25T13:00:14.133Z';
|
|
48207
|
+
__info__.hash = '564c094';
|