@odoo/o-spreadsheet 17.1.12 → 17.1.13

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.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 17.1.12
6
- * @date 2024-04-18T16:45:30.109Z
7
- * @hash 3e90776
5
+ * @version 17.1.13
6
+ * @date 2024-04-26T07:39:54.100Z
7
+ * @hash f2ea8c7
8
8
  */
9
9
 
10
10
  import { Component, useRef, onMounted, useEffect, onWillPatch, useState, onWillUpdateProps, onPatched, useComponent, useExternalListener, onWillUnmount, onWillStart, xml, useChildSubEnv, useSubEnv, markRaw } from '@odoo/owl';
@@ -27178,17 +27178,13 @@ class Composer extends Component {
27178
27178
  this.env.focusableElement.setFocusableElement(el);
27179
27179
  }
27180
27180
  this.contentHelper.updateEl(el);
27181
- this.processTokenAtCursor();
27182
27181
  });
27183
27182
  useEffect(() => {
27184
27183
  this.processContent();
27185
27184
  });
27186
- onPatched(() => {
27187
- // Required because typing '=SUM' and double-clicking another cell leaves ShowProvider/ShowDescription true
27188
- if (this.env.model.getters.getEditionMode() === "inactive") {
27189
- this.processTokenAtCursor();
27190
- }
27191
- });
27185
+ useEffect(() => {
27186
+ this.processTokenAtCursor();
27187
+ }, () => [this.env.model.getters.getEditionMode() !== "inactive"]);
27192
27188
  }
27193
27189
  // ---------------------------------------------------------------------------
27194
27190
  // Handlers
@@ -27626,6 +27622,7 @@ class Composer extends Component {
27626
27622
  const dataValidationAutocompleteValues = this.env.model.getters.getAutoCompleteDataValidationValues();
27627
27623
  if (!content.startsWith("=") && dataValidationAutocompleteValues.length) {
27628
27624
  this.showDataValidationAutocomplete(dataValidationAutocompleteValues);
27625
+ return;
27629
27626
  }
27630
27627
  if (content.startsWith("=")) {
27631
27628
  const token = this.env.model.getters.getTokenAtCursor();
@@ -34868,7 +34865,7 @@ class BordersPlugin extends CorePlugin {
34868
34865
  this.clearBorders(cmd.sheetId, cmd.target);
34869
34866
  break;
34870
34867
  case "REMOVE_COLUMNS_ROWS":
34871
- for (let el of cmd.elements) {
34868
+ for (let el of [...cmd.elements].sort((a, b) => b - a)) {
34872
34869
  if (cmd.dimension === "COL") {
34873
34870
  this.shiftBordersHorizontally(cmd.sheetId, el + 1, -1);
34874
34871
  }
@@ -40624,6 +40621,353 @@ class CompilationParametersBuilder {
40624
40621
  }
40625
40622
  }
40626
40623
 
40624
+ /**
40625
+ * ####################################################
40626
+ * # INTRODUCTION
40627
+ * ####################################################
40628
+ *
40629
+ * This file contain the function recomputeZones.
40630
+ * This function try to recompute in a performant way
40631
+ * an ensemble of zones possibly overlapping to avoid
40632
+ * overlapping and to reduce the number of zones.
40633
+ *
40634
+ * It also allows to remove some zones from the ensemble.
40635
+ *
40636
+ * In the following example, 2 zones are overlapping.
40637
+ * Applying recomputeZones will return zones without
40638
+ * overlapping:
40639
+ *
40640
+ * ["B3:D4", "D2:E3"] ["B3:C4", "D2:D4", "E2:E3"]
40641
+ *
40642
+ * A B C D E A B C D E
40643
+ * 1 ___ 1 ___
40644
+ * 2 ___|_ | 2 ___| | |
40645
+ * 3 | |_|_| ---> 3 | | |_|
40646
+ * 4 |_____| 4 |___|_|
40647
+ * 6 6
40648
+ * 7 7
40649
+ *
40650
+ *
40651
+ * In the following example, 2 zones are contiguous.
40652
+ * Applying recomputeZones will return only one zone:
40653
+ *
40654
+ * ["B2:B3", "C2:D3"] ["B2:D3"]
40655
+ *
40656
+ * A B C D E A B C D E
40657
+ * 1 _ ___ 1 _____
40658
+ * 2 | | | ---> 2 | |
40659
+ * 3 |_|___| 3 |_____|
40660
+ * 4 4
40661
+ *
40662
+ *
40663
+ * In the following example, we want to remove a zone
40664
+ * from the ensemble. Applying recomputeZones will
40665
+ * return the ensemble without the zone to remove:
40666
+ *
40667
+ * remove ["C3:D3"] ["B2:B4", "C2:D2",
40668
+ * "C4:D4", "E2:E4"]
40669
+ *
40670
+ * A B C D E F A B C D E F
40671
+ * 1 _______ 1 _______
40672
+ * 2 | | ---> 2 | |___| |
40673
+ * 3 | xxx | 3 | |___| |
40674
+ * 4 |_______| 4 |_|___|_|
40675
+ * 5 5
40676
+ *
40677
+ *
40678
+ * The exercise seems simple when we have only 2 zones.
40679
+ * But with n zones and in a performant way, we want to
40680
+ * avoid comparing each zone with all the others.
40681
+ *
40682
+ *
40683
+ * ####################################################
40684
+ * # Methodological approach
40685
+ * ####################################################
40686
+ *
40687
+ * The methodological approach to avoid comparing each
40688
+ * zone with all the others is to use a data structure
40689
+ * that allow to quickly find which zones are
40690
+ * overlapping with any other given zone.
40691
+ *
40692
+ * Here the idea is to profile the zones at the columns level.
40693
+ *
40694
+ * To do that, we propose to use a data structure
40695
+ * composed of 2 parts:
40696
+ * - profilesStartingPosition: a sorted number array
40697
+ * indicating on which columns a new profile begins.
40698
+ * - profiles: a map where the key is a column
40699
+ * position (from profilesStartingPosition) and the
40700
+ * value is a sorted number array representing a
40701
+ * profile.
40702
+ *
40703
+ *
40704
+ * See the following example: here profileStartingPosition
40705
+ * corresponds to [A,C,E,G,K]
40706
+ * A B C D E F G H I J K so with number [0,2,4,6,10]
40707
+ * 1 ' ' ' '
40708
+ * 2 ' ' '_______' here profile correspond
40709
+ * 3 '___' |_______| for A to []
40710
+ * 4 | | for C to [3, 5]
40711
+ * 5 |___| for E to []
40712
+ * 6 for G to [2, 3]
40713
+ * 7 for K to []
40714
+ *
40715
+ *
40716
+ * Now we can easily find which zones are overlapping
40717
+ * with a given zone. Suppose we want to add a new zone
40718
+ * D5:H6 to the ensemble:
40719
+ *
40720
+ * With a binary search of left and right
40721
+ * A B C D E F G H I J K on profilesStartingPosition, we can
40722
+ * 1 ' ' ' ' find the indexes of the profiles on which
40723
+ * 2 ' ' '_______' to apply a modification.
40724
+ * 3 '___' |_______|
40725
+ * 4 | _|_______ Here we will:
40726
+ * 5 |_|_| | - add a new profile in D --> become [3, 6]
40727
+ * 6 |_________| - modify the profile in E --> become [4, 6]
40728
+ * 7 - modify the profile in G --> become [2, 3, 4, 6]
40729
+ * - add a new profile in I --> become [8, 10]
40730
+ *
40731
+ * See below the result:
40732
+ *
40733
+ * Note the particularity of the profile
40734
+ * A B C D E F G H I J K for G: it will correspond to [2, 3, 4, 6]
40735
+ * 1 ' ' ' ' ' '
40736
+ * 2 ' ' ' '___'___' To know how to modify the profile (add a
40737
+ * 3 '_'_' |___|___| zone or remove it) we do a binary
40738
+ * 4 | | |___ ___ search of the top and bottom value on the
40739
+ * 5 |_| | | | profile array. Depending on the result index
40740
+ * 6 |_|___|___| parity (odd or even), because zone boundaries
40741
+ * 7 go by pairs, we know if we are in a zone or
40742
+ * not and how operate.
40743
+ */
40744
+ /**
40745
+ * Recompute the zone without the cells in toRemoveZones and avoid overlapping.
40746
+ * This compute is particularly useful because after this function:
40747
+ * - you will find coordinate of a cell only once among all the zones
40748
+ * - the number of zones will be reduced to the minimum
40749
+ */
40750
+ function futureRecomputeZones(zones, zonesToRemove = []) {
40751
+ const profilesStartingPosition = [0];
40752
+ const profiles = new Map([[0, []]]);
40753
+ modifyProfiles(profilesStartingPosition, profiles, zones, false);
40754
+ modifyProfiles(profilesStartingPosition, profiles, zonesToRemove, true);
40755
+ return constructZonesFromProfiles(profilesStartingPosition, profiles);
40756
+ }
40757
+ function modifyProfiles(// export for testing only
40758
+ profilesStartingPosition, profiles, zones, toRemove = false) {
40759
+ for (const zone of zones) {
40760
+ const leftValue = zone.left;
40761
+ const rightValue = zone.right === undefined ? undefined : zone.right + 1;
40762
+ const leftIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, leftValue, true, 0);
40763
+ const rightIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, rightValue, false, leftIndex);
40764
+ for (let i = leftIndex; i <= rightIndex; i++) {
40765
+ const profile = profiles.get(profilesStartingPosition[i]);
40766
+ modifyProfile(profile, zone, toRemove);
40767
+ }
40768
+ // maybe this part cost in performance, and maybe it's not necessary (depending on the use case). To be checked
40769
+ removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex);
40770
+ }
40771
+ }
40772
+ function findIndexAndCreateProfile(profilesStartingPosition, profiles, value, searchLeft, startIndex) {
40773
+ if (value === undefined) {
40774
+ // this is only the case when the value correspond to a bottom value that could be undefined
40775
+ return profilesStartingPosition.length - 1;
40776
+ }
40777
+ const predecessorIndex = binaryPredecessorSearch(profilesStartingPosition, value, startIndex);
40778
+ if (value != profilesStartingPosition[predecessorIndex]) {
40779
+ // mean that the value is not ending/starting at the same position as the previous/next profile
40780
+ // --> it's a new profile
40781
+ // --> we need to add it
40782
+ profilesStartingPosition.splice(predecessorIndex + 1, 0, value);
40783
+ // suppose the we want to add the for the left value
40784
+ // following profile following zone: 'C', the predecessor index
40785
+ // for B: [1, 3] "C3:D4" correspond to 'B'.
40786
+ // The next line code will
40787
+ // A B C D A B C D copy the profile of 'B'
40788
+ // 1 '___' 1 '___' to 'C'. In the rest of the
40789
+ // 2 | | ---> 2 | _|_ process the 'modifyProfile'
40790
+ // 3 |___| 3 |_|_| | function will adapt the waiting
40791
+ // 4 4 |___| 'C' profile [1, 3] to the
40792
+ // correct 'C' profile [1, 4]
40793
+ profiles.set(value, [...profiles.get(profilesStartingPosition[predecessorIndex])]);
40794
+ return searchLeft ? predecessorIndex + 1 : predecessorIndex;
40795
+ }
40796
+ return searchLeft ? predecessorIndex : predecessorIndex - 1;
40797
+ }
40798
+ /**
40799
+ * Suppose the following Suppose we want to add We want to have the
40800
+ * profile: the following zone: following profile:
40801
+ *
40802
+ * A B C D E F A B C D E F A B C D E F
40803
+ * 1 '___' 1 ' ' 1 '___'
40804
+ * 2 |___| 2 '___' 2 | |
40805
+ * 3 ' ' 3 | | 3 | |
40806
+ * 4 '___' --> 4 | | --> 4 | |
40807
+ * 6 | | 6 |___| 6 | |
40808
+ * 7 |___| 7 7 |___|
40809
+ * 8 8 8
40810
+ *
40811
+ * the profile for 'C' the top zone correspond Here [2, 3, 5, 8] with [3, 7]
40812
+ * corresponds to: to 3 and the bottom zone would be merged into [2, 8]
40813
+ * ____ ____ correspond to 6
40814
+ * [2, 3, 5, 8] would be the profile: The difficulty of modify profile
40815
+ * ____ is to know what must be deleted
40816
+ * Note that the 'filled [3, 7] and what must be added to the
40817
+ * zone' are always between existing profile.
40818
+ * an even index and its
40819
+ * next index
40820
+ *
40821
+ */
40822
+ function modifyProfile(profile, zone, toRemove = false) {
40823
+ const topValue = zone.top;
40824
+ const bottomValue = zone.bottom === undefined ? undefined : zone.bottom + 1;
40825
+ const newPoints = [];
40826
+ // Case we want to add a zone to the profile:
40827
+ // - If the top predecessor index `topPredIndex` is even, it means the top of the zone is already positioned on a filled zone
40828
+ // so we don't need to add it to the profile. we can keep in reference the index of the predecessor.
40829
+ // - If it is odd, it means the top of the zone must be the beginning of a filled zone.
40830
+ // so we can keep the index of the top position
40831
+ // Case we want to remove a zone from the profile: it's the opposite of the previous case
40832
+ const topPredIndex = binaryPredecessorSearch(profile, topValue, 0, false);
40833
+ if ((topPredIndex % 2 !== 0 && !toRemove) || (topPredIndex % 2 === 0 && toRemove)) {
40834
+ newPoints.push(topValue);
40835
+ }
40836
+ if (bottomValue === undefined) {
40837
+ // The following two code lines will not impact the final result,
40838
+ // but they will impact the intermediate profile.
40839
+ // We keep them for performance reason
40840
+ profile.splice(topPredIndex + 1);
40841
+ profile.push(...newPoints);
40842
+ return;
40843
+ }
40844
+ // Case we want to add a zone to the profile:
40845
+ // - If the bottom successor index `bottomSuccIndex` is even, it means the bottom of the zone must be the ending of a filled zone
40846
+ // so we can keep the index of the bottom position.
40847
+ // - If it is odd, it means the bottom of the zone is already positioned on a filled zone
40848
+ // so we don't need to add it to the profile. we can keep in reference the index of the successor
40849
+ // Case we want to remove a zone from the profile: it's the opposite of the previous case
40850
+ const bottomSuccIndex = binarySuccessorSearch(profile, bottomValue, 0, false);
40851
+ if ((bottomSuccIndex % 2 === 0 && !toRemove) || (bottomSuccIndex % 2 !== 0 && toRemove)) {
40852
+ newPoints.push(bottomValue);
40853
+ }
40854
+ // add the top and bottom value to the profile and
40855
+ // remove all information between the top and bottom index
40856
+ profile.splice(topPredIndex + 1, bottomSuccIndex - topPredIndex - 1, ...newPoints);
40857
+ }
40858
+ function removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex) {
40859
+ const start = leftIndex - 1 === -1 ? 0 : leftIndex - 1;
40860
+ const end = rightIndex === profilesStartingPosition.length - 1 ? rightIndex : rightIndex + 1;
40861
+ for (let i = end; i > start; i--) {
40862
+ if (deepEqualsArray(profiles.get(profilesStartingPosition[i]), profiles.get(profilesStartingPosition[i - 1]))) {
40863
+ profiles.delete(profilesStartingPosition[i]);
40864
+ profilesStartingPosition.splice(i, 1);
40865
+ }
40866
+ }
40867
+ }
40868
+ function constructZonesFromProfiles(profilesStartingPosition, profiles) {
40869
+ const mergedZone = [];
40870
+ let pendingZones = [];
40871
+ for (let colIndex = 0; colIndex < profilesStartingPosition.length; colIndex++) {
40872
+ const left = profilesStartingPosition[colIndex];
40873
+ const profile = profiles.get(left);
40874
+ if (!profile || profile.length === 0) {
40875
+ mergedZone.push(...pendingZones);
40876
+ pendingZones = [];
40877
+ continue;
40878
+ }
40879
+ let right = profilesStartingPosition[colIndex + 1];
40880
+ if (right !== undefined) {
40881
+ right--;
40882
+ }
40883
+ const nextPendingZones = [];
40884
+ for (let i = 0; i < profile.length; i += 2) {
40885
+ const top = profile[i];
40886
+ let bottom = profile[i + 1];
40887
+ if (bottom !== undefined) {
40888
+ bottom--;
40889
+ }
40890
+ const profileZone = {
40891
+ top,
40892
+ left,
40893
+ bottom,
40894
+ right,
40895
+ hasHeader: (bottom === undefined && top !== 0) || (right === undefined && left !== 0),
40896
+ };
40897
+ let findCorrespondingZone = false;
40898
+ for (let j = pendingZones.length - 1; j >= 0; j--) {
40899
+ const pendingZone = pendingZones[j];
40900
+ if (pendingZone.top === profileZone.top && pendingZone.bottom === profileZone.bottom) {
40901
+ pendingZone.right = profileZone.right;
40902
+ pendingZones.splice(j, 1);
40903
+ nextPendingZones.push(pendingZone);
40904
+ findCorrespondingZone = true;
40905
+ break;
40906
+ }
40907
+ }
40908
+ if (!findCorrespondingZone) {
40909
+ nextPendingZones.push(profileZone);
40910
+ }
40911
+ }
40912
+ mergedZone.push(...pendingZones);
40913
+ pendingZones = nextPendingZones;
40914
+ }
40915
+ mergedZone.push(...pendingZones);
40916
+ return mergedZone;
40917
+ }
40918
+ function binaryPredecessorSearch(arr, val, start = 0, matchEqual = true) {
40919
+ let end = arr.length - 1;
40920
+ let result = -1;
40921
+ while (start <= end) {
40922
+ const mid = Math.floor((start + end) / 2);
40923
+ if (arr[mid] === val && matchEqual) {
40924
+ return mid;
40925
+ }
40926
+ else if (arr[mid] < val) {
40927
+ result = mid;
40928
+ start = mid + 1;
40929
+ }
40930
+ else {
40931
+ end = mid - 1;
40932
+ }
40933
+ }
40934
+ return result;
40935
+ }
40936
+ function binarySuccessorSearch(arr, val, start = 0, matchEqual = true) {
40937
+ let end = arr.length - 1;
40938
+ let result = arr.length;
40939
+ while (start <= end) {
40940
+ const mid = Math.floor((start + end) / 2);
40941
+ if (arr[mid] === val && matchEqual) {
40942
+ return mid;
40943
+ }
40944
+ else if (arr[mid] > val) {
40945
+ result = mid;
40946
+ end = mid - 1;
40947
+ }
40948
+ else {
40949
+ start = mid + 1;
40950
+ }
40951
+ }
40952
+ return result;
40953
+ }
40954
+ /**
40955
+ * Compares two arrays.
40956
+ * For performance reasons, this function is to be preferred
40957
+ * to 'deepEquals' in the case we know that the inputs are arrays.
40958
+ */
40959
+ function deepEqualsArray(arr1, arr2) {
40960
+ if (arr1.length !== arr2.length) {
40961
+ return false;
40962
+ }
40963
+ for (let i = 0; i < arr1.length; i++) {
40964
+ if (!deepEquals(arr1[i], arr2[i])) {
40965
+ return false;
40966
+ }
40967
+ }
40968
+ return true;
40969
+ }
40970
+
40627
40971
  function quickselect(arr, k, left, right, compare) {
40628
40972
  quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);
40629
40973
  }
@@ -41387,7 +41731,7 @@ class FormulaDependencyGraph {
41387
41731
  }
41388
41732
  }
41389
41733
  /**
41390
- * Return the cell and all cells that depend on it,
41734
+ * Return all the cells that depend on the provided ranges,
41391
41735
  * in the correct order they should be evaluated.
41392
41736
  * This is called a topological ordering (excluding cycles)
41393
41737
  */
@@ -41398,11 +41742,20 @@ class FormulaDependencyGraph {
41398
41742
  const range = queue.pop();
41399
41743
  visited.addMany(this.encoder.encodeBoundingBox(range));
41400
41744
  const impactedPositionIds = this.rTree.search(range).map((dep) => dep.data);
41745
+ const nextInQueue = {};
41401
41746
  for (const positionId of impactedPositionIds) {
41402
41747
  if (!visited.has(positionId)) {
41403
- queue.push(this.encoder.decodeToBoundingBox(positionId));
41748
+ const { sheetId, zone } = this.encoder.decodeToBoundingBox(positionId);
41749
+ if (!nextInQueue[sheetId]) {
41750
+ nextInQueue[sheetId] = [];
41751
+ }
41752
+ nextInQueue[sheetId].push(zone);
41404
41753
  }
41405
41754
  }
41755
+ for (const sheetId in nextInQueue) {
41756
+ const zones = futureRecomputeZones(nextInQueue[sheetId]);
41757
+ queue.push(...zones.map((zone) => ({ sheetId, zone })));
41758
+ }
41406
41759
  }
41407
41760
  visited.deleteMany(ranges.flatMap((r) => this.encoder.encodeBoundingBox(r)));
41408
41761
  return visited;
@@ -41560,7 +41913,7 @@ class Evaluator {
41560
41913
  }
41561
41914
  if (!content) {
41562
41915
  // The previous content could have blocked some array formulas
41563
- impactedPositionIds.addMany(this.getArrayFormulasBlockedByOrSpreadingOn(positionId));
41916
+ impactedPositionIds.addMany(this.getArrayFormulasBlockedBy(positionId));
41564
41917
  }
41565
41918
  }
41566
41919
  return impactedPositionIds;
@@ -41608,13 +41961,22 @@ class Evaluator {
41608
41961
  }
41609
41962
  return positionIds;
41610
41963
  }
41611
- getArrayFormulasBlockedByOrSpreadingOn(positionId) {
41964
+ /**
41965
+ * Return the position of formulas blocked by the given position
41966
+ * as well as all their dependencies.
41967
+ */
41968
+ getArrayFormulasBlockedBy(positionId) {
41612
41969
  if (!this.spreadingRelations.hasArrayFormulaResult(positionId)) {
41613
41970
  return [];
41614
41971
  }
41615
41972
  const arrayFormulas = this.spreadingRelations.getFormulaPositionsSpreadingOn(positionId);
41616
41973
  const cells = new JetSet(arrayFormulas);
41617
- cells.addMany(this.getCellsDependingOn(arrayFormulas));
41974
+ const arrayFormulaPositionId = this.getArrayFormulaSpreadingOnId(positionId);
41975
+ if (arrayFormulaPositionId) {
41976
+ // ignore the formula spreading on the position. Keep only the blocked ones
41977
+ cells.delete(arrayFormulaPositionId);
41978
+ }
41979
+ cells.addMany(this.getCellsDependingOn(cells));
41618
41980
  return cells;
41619
41981
  }
41620
41982
  nextPositionsToUpdate = new JetSet();
@@ -41768,7 +42130,7 @@ class Evaluator {
41768
42130
  }
41769
42131
  this.evaluatedCells.delete(child);
41770
42132
  this.nextPositionsToUpdate.addMany(this.getCellsDependingOn([child]));
41771
- this.nextPositionsToUpdate.addMany(this.getArrayFormulasBlockedByOrSpreadingOn(child));
42133
+ this.nextPositionsToUpdate.addMany(this.getArrayFormulasBlockedBy(child));
41772
42134
  }
41773
42135
  this.spreadingRelations.removeNode(positionId);
41774
42136
  }
@@ -57318,6 +57680,6 @@ const constants = {
57318
57680
  export { AbstractChart, CellErrorType, CommandResult, CorePlugin, DispatchResult, EvaluationError, Model, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, UIPlugin, __info__, addFunction, astToFormula, compile, compileTokens, components, constants, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, hooks, invalidateCFEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, tokenize };
57319
57681
 
57320
57682
 
57321
- __info__.version = "17.1.12";
57322
- __info__.date = "2024-04-18T16:45:30.109Z";
57323
- __info__.hash = "3e90776";
57683
+ __info__.version = "17.1.13";
57684
+ __info__.date = "2024-04-26T07:39:54.100Z";
57685
+ __info__.hash = "f2ea8c7";