@odoo/o-spreadsheet 17.2.4 → 17.2.5

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.
@@ -3,9 +3,9 @@
3
3
  /**
4
4
  * This file is generated by o-spreadsheet build tools. Do not edit it.
5
5
  * @see https://github.com/odoo/o-spreadsheet
6
- * @version 17.2.4
7
- * @date 2024-04-18T16:41:38.407Z
8
- * @hash 0c66038
6
+ * @version 17.2.5
7
+ * @date 2024-04-26T07:41:13.193Z
8
+ * @hash a730f5c
9
9
  */
10
10
 
11
11
  'use strict';
@@ -32073,17 +32073,13 @@ class Composer extends owl.Component {
32073
32073
  this.DOMFocusableElementStore.setFocusableElement(el);
32074
32074
  }
32075
32075
  this.contentHelper.updateEl(el);
32076
- this.processTokenAtCursor();
32077
32076
  });
32078
32077
  owl.useEffect(() => {
32079
32078
  this.processContent();
32080
32079
  });
32081
- owl.onPatched(() => {
32082
- // Required because typing '=SUM' and double-clicking another cell leaves ShowProvider/ShowDescription true
32083
- if (this.composerStore.editionMode === "inactive") {
32084
- this.processTokenAtCursor();
32085
- }
32086
- });
32080
+ owl.useEffect(() => {
32081
+ this.processTokenAtCursor();
32082
+ }, () => [this.composerStore.editionMode !== "inactive"]);
32087
32083
  }
32088
32084
  // ---------------------------------------------------------------------------
32089
32085
  // Handlers
@@ -40380,7 +40376,7 @@ class BordersPlugin extends CorePlugin {
40380
40376
  this.clearBorders(cmd.sheetId, cmd.target);
40381
40377
  break;
40382
40378
  case "REMOVE_COLUMNS_ROWS":
40383
- for (let el of cmd.elements) {
40379
+ for (let el of [...cmd.elements].sort((a, b) => b - a)) {
40384
40380
  if (cmd.dimension === "COL") {
40385
40381
  this.shiftBordersHorizontally(cmd.sheetId, el + 1, -1);
40386
40382
  }
@@ -45919,6 +45915,353 @@ class CompilationParametersBuilder {
45919
45915
  }
45920
45916
  }
45921
45917
 
45918
+ /**
45919
+ * ####################################################
45920
+ * # INTRODUCTION
45921
+ * ####################################################
45922
+ *
45923
+ * This file contain the function recomputeZones.
45924
+ * This function try to recompute in a performant way
45925
+ * an ensemble of zones possibly overlapping to avoid
45926
+ * overlapping and to reduce the number of zones.
45927
+ *
45928
+ * It also allows to remove some zones from the ensemble.
45929
+ *
45930
+ * In the following example, 2 zones are overlapping.
45931
+ * Applying recomputeZones will return zones without
45932
+ * overlapping:
45933
+ *
45934
+ * ["B3:D4", "D2:E3"] ["B3:C4", "D2:D4", "E2:E3"]
45935
+ *
45936
+ * A B C D E A B C D E
45937
+ * 1 ___ 1 ___
45938
+ * 2 ___|_ | 2 ___| | |
45939
+ * 3 | |_|_| ---> 3 | | |_|
45940
+ * 4 |_____| 4 |___|_|
45941
+ * 6 6
45942
+ * 7 7
45943
+ *
45944
+ *
45945
+ * In the following example, 2 zones are contiguous.
45946
+ * Applying recomputeZones will return only one zone:
45947
+ *
45948
+ * ["B2:B3", "C2:D3"] ["B2:D3"]
45949
+ *
45950
+ * A B C D E A B C D E
45951
+ * 1 _ ___ 1 _____
45952
+ * 2 | | | ---> 2 | |
45953
+ * 3 |_|___| 3 |_____|
45954
+ * 4 4
45955
+ *
45956
+ *
45957
+ * In the following example, we want to remove a zone
45958
+ * from the ensemble. Applying recomputeZones will
45959
+ * return the ensemble without the zone to remove:
45960
+ *
45961
+ * remove ["C3:D3"] ["B2:B4", "C2:D2",
45962
+ * "C4:D4", "E2:E4"]
45963
+ *
45964
+ * A B C D E F A B C D E F
45965
+ * 1 _______ 1 _______
45966
+ * 2 | | ---> 2 | |___| |
45967
+ * 3 | xxx | 3 | |___| |
45968
+ * 4 |_______| 4 |_|___|_|
45969
+ * 5 5
45970
+ *
45971
+ *
45972
+ * The exercise seems simple when we have only 2 zones.
45973
+ * But with n zones and in a performant way, we want to
45974
+ * avoid comparing each zone with all the others.
45975
+ *
45976
+ *
45977
+ * ####################################################
45978
+ * # Methodological approach
45979
+ * ####################################################
45980
+ *
45981
+ * The methodological approach to avoid comparing each
45982
+ * zone with all the others is to use a data structure
45983
+ * that allow to quickly find which zones are
45984
+ * overlapping with any other given zone.
45985
+ *
45986
+ * Here the idea is to profile the zones at the columns level.
45987
+ *
45988
+ * To do that, we propose to use a data structure
45989
+ * composed of 2 parts:
45990
+ * - profilesStartingPosition: a sorted number array
45991
+ * indicating on which columns a new profile begins.
45992
+ * - profiles: a map where the key is a column
45993
+ * position (from profilesStartingPosition) and the
45994
+ * value is a sorted number array representing a
45995
+ * profile.
45996
+ *
45997
+ *
45998
+ * See the following example: here profileStartingPosition
45999
+ * corresponds to [A,C,E,G,K]
46000
+ * A B C D E F G H I J K so with number [0,2,4,6,10]
46001
+ * 1 ' ' ' '
46002
+ * 2 ' ' '_______' here profile correspond
46003
+ * 3 '___' |_______| for A to []
46004
+ * 4 | | for C to [3, 5]
46005
+ * 5 |___| for E to []
46006
+ * 6 for G to [2, 3]
46007
+ * 7 for K to []
46008
+ *
46009
+ *
46010
+ * Now we can easily find which zones are overlapping
46011
+ * with a given zone. Suppose we want to add a new zone
46012
+ * D5:H6 to the ensemble:
46013
+ *
46014
+ * With a binary search of left and right
46015
+ * A B C D E F G H I J K on profilesStartingPosition, we can
46016
+ * 1 ' ' ' ' find the indexes of the profiles on which
46017
+ * 2 ' ' '_______' to apply a modification.
46018
+ * 3 '___' |_______|
46019
+ * 4 | _|_______ Here we will:
46020
+ * 5 |_|_| | - add a new profile in D --> become [3, 6]
46021
+ * 6 |_________| - modify the profile in E --> become [4, 6]
46022
+ * 7 - modify the profile in G --> become [2, 3, 4, 6]
46023
+ * - add a new profile in I --> become [8, 10]
46024
+ *
46025
+ * See below the result:
46026
+ *
46027
+ * Note the particularity of the profile
46028
+ * A B C D E F G H I J K for G: it will correspond to [2, 3, 4, 6]
46029
+ * 1 ' ' ' ' ' '
46030
+ * 2 ' ' ' '___'___' To know how to modify the profile (add a
46031
+ * 3 '_'_' |___|___| zone or remove it) we do a binary
46032
+ * 4 | | |___ ___ search of the top and bottom value on the
46033
+ * 5 |_| | | | profile array. Depending on the result index
46034
+ * 6 |_|___|___| parity (odd or even), because zone boundaries
46035
+ * 7 go by pairs, we know if we are in a zone or
46036
+ * not and how operate.
46037
+ */
46038
+ /**
46039
+ * Recompute the zone without the cells in toRemoveZones and avoid overlapping.
46040
+ * This compute is particularly useful because after this function:
46041
+ * - you will find coordinate of a cell only once among all the zones
46042
+ * - the number of zones will be reduced to the minimum
46043
+ */
46044
+ function futureRecomputeZones(zones, zonesToRemove = []) {
46045
+ const profilesStartingPosition = [0];
46046
+ const profiles = new Map([[0, []]]);
46047
+ modifyProfiles(profilesStartingPosition, profiles, zones, false);
46048
+ modifyProfiles(profilesStartingPosition, profiles, zonesToRemove, true);
46049
+ return constructZonesFromProfiles(profilesStartingPosition, profiles);
46050
+ }
46051
+ function modifyProfiles(// export for testing only
46052
+ profilesStartingPosition, profiles, zones, toRemove = false) {
46053
+ for (const zone of zones) {
46054
+ const leftValue = zone.left;
46055
+ const rightValue = zone.right === undefined ? undefined : zone.right + 1;
46056
+ const leftIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, leftValue, true, 0);
46057
+ const rightIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, rightValue, false, leftIndex);
46058
+ for (let i = leftIndex; i <= rightIndex; i++) {
46059
+ const profile = profiles.get(profilesStartingPosition[i]);
46060
+ modifyProfile(profile, zone, toRemove);
46061
+ }
46062
+ // maybe this part cost in performance, and maybe it's not necessary (depending on the use case). To be checked
46063
+ removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex);
46064
+ }
46065
+ }
46066
+ function findIndexAndCreateProfile(profilesStartingPosition, profiles, value, searchLeft, startIndex) {
46067
+ if (value === undefined) {
46068
+ // this is only the case when the value correspond to a bottom value that could be undefined
46069
+ return profilesStartingPosition.length - 1;
46070
+ }
46071
+ const predecessorIndex = binaryPredecessorSearch(profilesStartingPosition, value, startIndex);
46072
+ if (value != profilesStartingPosition[predecessorIndex]) {
46073
+ // mean that the value is not ending/starting at the same position as the previous/next profile
46074
+ // --> it's a new profile
46075
+ // --> we need to add it
46076
+ profilesStartingPosition.splice(predecessorIndex + 1, 0, value);
46077
+ // suppose the we want to add the for the left value
46078
+ // following profile following zone: 'C', the predecessor index
46079
+ // for B: [1, 3] "C3:D4" correspond to 'B'.
46080
+ // The next line code will
46081
+ // A B C D A B C D copy the profile of 'B'
46082
+ // 1 '___' 1 '___' to 'C'. In the rest of the
46083
+ // 2 | | ---> 2 | _|_ process the 'modifyProfile'
46084
+ // 3 |___| 3 |_|_| | function will adapt the waiting
46085
+ // 4 4 |___| 'C' profile [1, 3] to the
46086
+ // correct 'C' profile [1, 4]
46087
+ profiles.set(value, [...profiles.get(profilesStartingPosition[predecessorIndex])]);
46088
+ return searchLeft ? predecessorIndex + 1 : predecessorIndex;
46089
+ }
46090
+ return searchLeft ? predecessorIndex : predecessorIndex - 1;
46091
+ }
46092
+ /**
46093
+ * Suppose the following Suppose we want to add We want to have the
46094
+ * profile: the following zone: following profile:
46095
+ *
46096
+ * A B C D E F A B C D E F A B C D E F
46097
+ * 1 '___' 1 ' ' 1 '___'
46098
+ * 2 |___| 2 '___' 2 | |
46099
+ * 3 ' ' 3 | | 3 | |
46100
+ * 4 '___' --> 4 | | --> 4 | |
46101
+ * 6 | | 6 |___| 6 | |
46102
+ * 7 |___| 7 7 |___|
46103
+ * 8 8 8
46104
+ *
46105
+ * the profile for 'C' the top zone correspond Here [2, 3, 5, 8] with [3, 7]
46106
+ * corresponds to: to 3 and the bottom zone would be merged into [2, 8]
46107
+ * ____ ____ correspond to 6
46108
+ * [2, 3, 5, 8] would be the profile: The difficulty of modify profile
46109
+ * ____ is to know what must be deleted
46110
+ * Note that the 'filled [3, 7] and what must be added to the
46111
+ * zone' are always between existing profile.
46112
+ * an even index and its
46113
+ * next index
46114
+ *
46115
+ */
46116
+ function modifyProfile(profile, zone, toRemove = false) {
46117
+ const topValue = zone.top;
46118
+ const bottomValue = zone.bottom === undefined ? undefined : zone.bottom + 1;
46119
+ const newPoints = [];
46120
+ // Case we want to add a zone to the profile:
46121
+ // - If the top predecessor index `topPredIndex` is even, it means the top of the zone is already positioned on a filled zone
46122
+ // so we don't need to add it to the profile. we can keep in reference the index of the predecessor.
46123
+ // - If it is odd, it means the top of the zone must be the beginning of a filled zone.
46124
+ // so we can keep the index of the top position
46125
+ // Case we want to remove a zone from the profile: it's the opposite of the previous case
46126
+ const topPredIndex = binaryPredecessorSearch(profile, topValue, 0, false);
46127
+ if ((topPredIndex % 2 !== 0 && !toRemove) || (topPredIndex % 2 === 0 && toRemove)) {
46128
+ newPoints.push(topValue);
46129
+ }
46130
+ if (bottomValue === undefined) {
46131
+ // The following two code lines will not impact the final result,
46132
+ // but they will impact the intermediate profile.
46133
+ // We keep them for performance reason
46134
+ profile.splice(topPredIndex + 1);
46135
+ profile.push(...newPoints);
46136
+ return;
46137
+ }
46138
+ // Case we want to add a zone to the profile:
46139
+ // - If the bottom successor index `bottomSuccIndex` is even, it means the bottom of the zone must be the ending of a filled zone
46140
+ // so we can keep the index of the bottom position.
46141
+ // - If it is odd, it means the bottom of the zone is already positioned on a filled zone
46142
+ // so we don't need to add it to the profile. we can keep in reference the index of the successor
46143
+ // Case we want to remove a zone from the profile: it's the opposite of the previous case
46144
+ const bottomSuccIndex = binarySuccessorSearch(profile, bottomValue, 0, false);
46145
+ if ((bottomSuccIndex % 2 === 0 && !toRemove) || (bottomSuccIndex % 2 !== 0 && toRemove)) {
46146
+ newPoints.push(bottomValue);
46147
+ }
46148
+ // add the top and bottom value to the profile and
46149
+ // remove all information between the top and bottom index
46150
+ profile.splice(topPredIndex + 1, bottomSuccIndex - topPredIndex - 1, ...newPoints);
46151
+ }
46152
+ function removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex) {
46153
+ const start = leftIndex - 1 === -1 ? 0 : leftIndex - 1;
46154
+ const end = rightIndex === profilesStartingPosition.length - 1 ? rightIndex : rightIndex + 1;
46155
+ for (let i = end; i > start; i--) {
46156
+ if (deepEqualsArray(profiles.get(profilesStartingPosition[i]), profiles.get(profilesStartingPosition[i - 1]))) {
46157
+ profiles.delete(profilesStartingPosition[i]);
46158
+ profilesStartingPosition.splice(i, 1);
46159
+ }
46160
+ }
46161
+ }
46162
+ function constructZonesFromProfiles(profilesStartingPosition, profiles) {
46163
+ const mergedZone = [];
46164
+ let pendingZones = [];
46165
+ for (let colIndex = 0; colIndex < profilesStartingPosition.length; colIndex++) {
46166
+ const left = profilesStartingPosition[colIndex];
46167
+ const profile = profiles.get(left);
46168
+ if (!profile || profile.length === 0) {
46169
+ mergedZone.push(...pendingZones);
46170
+ pendingZones = [];
46171
+ continue;
46172
+ }
46173
+ let right = profilesStartingPosition[colIndex + 1];
46174
+ if (right !== undefined) {
46175
+ right--;
46176
+ }
46177
+ const nextPendingZones = [];
46178
+ for (let i = 0; i < profile.length; i += 2) {
46179
+ const top = profile[i];
46180
+ let bottom = profile[i + 1];
46181
+ if (bottom !== undefined) {
46182
+ bottom--;
46183
+ }
46184
+ const profileZone = {
46185
+ top,
46186
+ left,
46187
+ bottom,
46188
+ right,
46189
+ hasHeader: (bottom === undefined && top !== 0) || (right === undefined && left !== 0),
46190
+ };
46191
+ let findCorrespondingZone = false;
46192
+ for (let j = pendingZones.length - 1; j >= 0; j--) {
46193
+ const pendingZone = pendingZones[j];
46194
+ if (pendingZone.top === profileZone.top && pendingZone.bottom === profileZone.bottom) {
46195
+ pendingZone.right = profileZone.right;
46196
+ pendingZones.splice(j, 1);
46197
+ nextPendingZones.push(pendingZone);
46198
+ findCorrespondingZone = true;
46199
+ break;
46200
+ }
46201
+ }
46202
+ if (!findCorrespondingZone) {
46203
+ nextPendingZones.push(profileZone);
46204
+ }
46205
+ }
46206
+ mergedZone.push(...pendingZones);
46207
+ pendingZones = nextPendingZones;
46208
+ }
46209
+ mergedZone.push(...pendingZones);
46210
+ return mergedZone;
46211
+ }
46212
+ function binaryPredecessorSearch(arr, val, start = 0, matchEqual = true) {
46213
+ let end = arr.length - 1;
46214
+ let result = -1;
46215
+ while (start <= end) {
46216
+ const mid = Math.floor((start + end) / 2);
46217
+ if (arr[mid] === val && matchEqual) {
46218
+ return mid;
46219
+ }
46220
+ else if (arr[mid] < val) {
46221
+ result = mid;
46222
+ start = mid + 1;
46223
+ }
46224
+ else {
46225
+ end = mid - 1;
46226
+ }
46227
+ }
46228
+ return result;
46229
+ }
46230
+ function binarySuccessorSearch(arr, val, start = 0, matchEqual = true) {
46231
+ let end = arr.length - 1;
46232
+ let result = arr.length;
46233
+ while (start <= end) {
46234
+ const mid = Math.floor((start + end) / 2);
46235
+ if (arr[mid] === val && matchEqual) {
46236
+ return mid;
46237
+ }
46238
+ else if (arr[mid] > val) {
46239
+ result = mid;
46240
+ end = mid - 1;
46241
+ }
46242
+ else {
46243
+ start = mid + 1;
46244
+ }
46245
+ }
46246
+ return result;
46247
+ }
46248
+ /**
46249
+ * Compares two arrays.
46250
+ * For performance reasons, this function is to be preferred
46251
+ * to 'deepEquals' in the case we know that the inputs are arrays.
46252
+ */
46253
+ function deepEqualsArray(arr1, arr2) {
46254
+ if (arr1.length !== arr2.length) {
46255
+ return false;
46256
+ }
46257
+ for (let i = 0; i < arr1.length; i++) {
46258
+ if (!deepEquals(arr1[i], arr2[i])) {
46259
+ return false;
46260
+ }
46261
+ }
46262
+ return true;
46263
+ }
46264
+
45922
46265
  class PositionMap {
45923
46266
  map = {};
45924
46267
  set({ sheetId, col, row }, value) {
@@ -46717,7 +47060,7 @@ class FormulaDependencyGraph {
46717
47060
  }
46718
47061
  }
46719
47062
  /**
46720
- * Return the cell and all cells that depend on it,
47063
+ * Return all the cells that depend on the provided ranges,
46721
47064
  * in the correct order they should be evaluated.
46722
47065
  * This is called a topological ordering (excluding cycles)
46723
47066
  */
@@ -46728,11 +47071,19 @@ class FormulaDependencyGraph {
46728
47071
  const range = queue.pop();
46729
47072
  visited.addMany(positions(range.zone).map((position) => ({ sheetId: range.sheetId, ...position })));
46730
47073
  const impactedPositions = this.rTree.search(range).map((dep) => dep.data);
47074
+ const nextInQueue = {};
46731
47075
  for (const position of impactedPositions) {
46732
47076
  if (!visited.has(position)) {
46733
- queue.push({ sheetId: position.sheetId, zone: positionToZone(position) });
47077
+ if (!nextInQueue[position.sheetId]) {
47078
+ nextInQueue[position.sheetId] = [];
47079
+ }
47080
+ nextInQueue[position.sheetId].push(positionToZone(position));
46734
47081
  }
46735
47082
  }
47083
+ for (const sheetId in nextInQueue) {
47084
+ const zones = futureRecomputeZones(nextInQueue[sheetId]);
47085
+ queue.push(...zones.map((zone) => ({ sheetId, zone })));
47086
+ }
46736
47087
  }
46737
47088
  visited.deleteMany(ranges.flatMap((r) => positions(r.zone).map((position) => ({ sheetId: r.sheetId, ...position }))));
46738
47089
  return visited;
@@ -47071,7 +47422,7 @@ class Evaluator {
47071
47422
  }
47072
47423
  if (!content) {
47073
47424
  // The previous content could have blocked some array formulas
47074
- impactedPositions.addMany(this.getArrayFormulasBlockedByOrSpreadingOn(position));
47425
+ impactedPositions.addMany(this.getArrayFormulasBlockedBy(position));
47075
47426
  }
47076
47427
  }
47077
47428
  return impactedPositions;
@@ -47114,14 +47465,23 @@ class Evaluator {
47114
47465
  positions.fillAllPositions();
47115
47466
  return positions;
47116
47467
  }
47117
- getArrayFormulasBlockedByOrSpreadingOn(position) {
47468
+ /**
47469
+ * Return the position of formulas blocked by the given position
47470
+ * as well as all their dependencies.
47471
+ */
47472
+ getArrayFormulasBlockedBy(position) {
47118
47473
  if (!this.spreadingRelations.hasArrayFormulaResult(position)) {
47119
47474
  return [];
47120
47475
  }
47121
47476
  const arrayFormulas = this.spreadingRelations.getFormulaPositionsSpreadingOn(position);
47122
47477
  const positions = this.createEmptyPositionSet();
47123
47478
  positions.addMany(arrayFormulas);
47124
- positions.addMany(this.getCellsDependingOn(arrayFormulas));
47479
+ const arrayFormulaPosition = this.getArrayFormulaSpreadingOn(position);
47480
+ if (arrayFormulaPosition) {
47481
+ // ignore the formula spreading on the position. Keep only the blocked ones
47482
+ positions.delete(arrayFormulaPosition);
47483
+ }
47484
+ positions.addMany(this.getCellsDependingOn(positions));
47125
47485
  return positions;
47126
47486
  }
47127
47487
  nextPositionsToUpdate = new PositionSet({});
@@ -47261,7 +47621,7 @@ class Evaluator {
47261
47621
  }
47262
47622
  this.evaluatedCells.delete(child);
47263
47623
  this.nextPositionsToUpdate.addMany(this.getCellsDependingOn([child]));
47264
- this.nextPositionsToUpdate.addMany(this.getArrayFormulasBlockedByOrSpreadingOn(child));
47624
+ this.nextPositionsToUpdate.addMany(this.getArrayFormulasBlockedBy(child));
47265
47625
  }
47266
47626
  this.spreadingRelations.removeNode(position);
47267
47627
  }
@@ -60240,6 +60600,6 @@ exports.tokenColors = tokenColors;
60240
60600
  exports.tokenize = tokenize;
60241
60601
 
60242
60602
 
60243
- __info__.version = "17.2.4";
60244
- __info__.date = "2024-04-18T16:41:38.407Z";
60245
- __info__.hash = "0c66038";
60603
+ __info__.version = "17.2.5";
60604
+ __info__.date = "2024-04-26T07:41:13.193Z";
60605
+ __info__.hash = "a730f5c";