@odoo/o-spreadsheet 17.0.5 → 17.0.7

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.0.5
6
- * @date 2023-12-05T09:51:44.657Z
7
- * @hash 2480a5d
5
+ * @version 17.0.7
6
+ * @date 2024-01-12T13:42:04.654Z
7
+ * @hash cadb817
8
8
  */
9
9
 
10
10
  'use strict';
@@ -331,13 +331,6 @@ const FONT_SIZES = [6, 7, 8, 9, 10, 11, 12, 14, 18, 24, 36];
331
331
  //------------------------------------------------------------------------------
332
332
  // Miscellaneous
333
333
  //------------------------------------------------------------------------------
334
- /**
335
- * Stringify an object, like JSON.stringify, except that the first level of keys
336
- * is ordered.
337
- */
338
- function stringify(obj) {
339
- return JSON.stringify(obj, Object.keys(obj).sort());
340
- }
341
334
  /**
342
335
  * Remove quotes from a quoted string
343
336
  * ```js
@@ -560,7 +553,7 @@ function isObjectEmptyRecursive(argument) {
560
553
  */
561
554
  function getItemId(item, itemsDic) {
562
555
  for (let [key, value] of Object.entries(itemsDic)) {
563
- if (stringify(value) === stringify(item)) {
556
+ if (deepEquals(value, item)) {
564
557
  return parseInt(key, 10);
565
558
  }
566
559
  }
@@ -1210,16 +1203,84 @@ function toXC(col, row, rangePart = { colFixed: false, rowFixed: false }) {
1210
1203
  // -----------------------------------------------------------------------------
1211
1204
  // Date Type
1212
1205
  // -----------------------------------------------------------------------------
1206
+ /**
1207
+ * A DateTime object that can be used to manipulate spreadsheet dates.
1208
+ * Conceptually, a spreadsheet date is simply a number with a date format,
1209
+ * and it is timezone-agnostic.
1210
+ * This DateTime object consistently uses UTC time to represent a naive date and time.
1211
+ */
1212
+ class DateTime {
1213
+ jsDate;
1214
+ constructor(year, month, day, hours = 0, minutes = 0, seconds = 0) {
1215
+ this.jsDate = new Date(Date.UTC(year, month, day, hours, minutes, seconds, 0));
1216
+ }
1217
+ static fromTimestamp(timestamp) {
1218
+ const date = new Date(timestamp);
1219
+ return new DateTime(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());
1220
+ }
1221
+ static now() {
1222
+ const now = new Date();
1223
+ return new DateTime(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds());
1224
+ }
1225
+ toString() {
1226
+ return this.jsDate.toString();
1227
+ }
1228
+ toLocaleDateString() {
1229
+ return this.jsDate.toLocaleDateString();
1230
+ }
1231
+ getTime() {
1232
+ return this.jsDate.getTime();
1233
+ }
1234
+ getFullYear() {
1235
+ return this.jsDate.getUTCFullYear();
1236
+ }
1237
+ getMonth() {
1238
+ return this.jsDate.getUTCMonth();
1239
+ }
1240
+ getDate() {
1241
+ return this.jsDate.getUTCDate();
1242
+ }
1243
+ getDay() {
1244
+ return this.jsDate.getUTCDay();
1245
+ }
1246
+ getHours() {
1247
+ return this.jsDate.getUTCHours();
1248
+ }
1249
+ getMinutes() {
1250
+ return this.jsDate.getUTCMinutes();
1251
+ }
1252
+ getSeconds() {
1253
+ return this.jsDate.getUTCSeconds();
1254
+ }
1255
+ setFullYear(year) {
1256
+ return this.jsDate.setUTCFullYear(year);
1257
+ }
1258
+ setMonth(month) {
1259
+ return this.jsDate.setUTCMonth(month);
1260
+ }
1261
+ setDate(date) {
1262
+ return this.jsDate.setUTCDate(date);
1263
+ }
1264
+ setHours(hours) {
1265
+ return this.jsDate.setUTCHours(hours);
1266
+ }
1267
+ setMinutes(minutes) {
1268
+ return this.jsDate.setUTCMinutes(minutes);
1269
+ }
1270
+ setSeconds(seconds) {
1271
+ return this.jsDate.setUTCSeconds(seconds);
1272
+ }
1273
+ }
1213
1274
  // -----------------------------------------------------------------------------
1214
1275
  // Parsing
1215
1276
  // -----------------------------------------------------------------------------
1216
- const INITIAL_1900_DAY = new Date(1899, 11, 30);
1277
+ const INITIAL_1900_DAY = new DateTime(1899, 11, 30);
1217
1278
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
1218
1279
  const CURRENT_MILLENIAL = 2000; // note: don't forget to update this in 2999
1219
- const CURRENT_YEAR = new Date().getFullYear();
1220
- const CURRENT_MONTH = new Date().getMonth();
1221
- const INITIAL_JS_DAY = new Date(0);
1222
- const DATE_JS_1900_OFFSET = INITIAL_JS_DAY - INITIAL_1900_DAY;
1280
+ const CURRENT_YEAR = DateTime.now().getFullYear();
1281
+ const CURRENT_MONTH = DateTime.now().getMonth();
1282
+ const INITIAL_JS_DAY = DateTime.fromTimestamp(0);
1283
+ const DATE_JS_1900_OFFSET = INITIAL_JS_DAY.getTime() - INITIAL_1900_DAY.getTime();
1223
1284
  const mdyDateRegexp = /^\d{1,2}(\/|-|\s)\d{1,2}((\/|-|\s)\d{1,4})?$/;
1224
1285
  const ymdDateRegexp = /^\d{3,4}(\/|-|\s)\d{1,2}(\/|-|\s)\d{1,2}$/;
1225
1286
  const dateSeparatorsRegex = /\/|-|\s/;
@@ -1280,7 +1341,7 @@ function _parseDateTime(str, locale) {
1280
1341
  return {
1281
1342
  value: date.value + time.value,
1282
1343
  format: date.format + " " + (time.format === "hhhh:mm:ss" ? "hh:mm:ss" : time.format),
1283
- jsDate: new Date(date.jsDate.getFullYear() + time.jsDate.getFullYear() - 1899, date.jsDate.getMonth() + time.jsDate.getMonth() - 11, date.jsDate.getDate() + time.jsDate.getDate() - 30, date.jsDate.getHours() + time.jsDate.getHours(), date.jsDate.getMinutes() + time.jsDate.getMinutes(), date.jsDate.getSeconds() + time.jsDate.getSeconds()),
1344
+ jsDate: new DateTime(date.jsDate.getFullYear() + time.jsDate.getFullYear() - 1899, date.jsDate.getMonth() + time.jsDate.getMonth() - 11, date.jsDate.getDate() + time.jsDate.getDate() - 30, date.jsDate.getHours() + time.jsDate.getHours(), date.jsDate.getMinutes() + time.jsDate.getMinutes(), date.jsDate.getSeconds() + time.jsDate.getSeconds()),
1284
1345
  };
1285
1346
  }
1286
1347
  return date || time;
@@ -1356,12 +1417,12 @@ function parseDate(parts, separator) {
1356
1417
  // month + 1: months are 0-indexed in JS
1357
1418
  const leadingZero = (monthStr?.length === 2 && month + 1 < 10) || (dayStr?.length === 2 && day < 10);
1358
1419
  const fullYear = yearStr?.length !== 2;
1359
- const jsDate = new Date(year, month, day);
1420
+ const jsDate = new DateTime(year, month, day);
1360
1421
  if (jsDate.getMonth() !== month || jsDate.getDate() !== day) {
1361
1422
  // invalid date
1362
1423
  return null;
1363
1424
  }
1364
- const delta = jsDate - INITIAL_1900_DAY;
1425
+ const delta = jsDate.getTime() - INITIAL_1900_DAY.getTime();
1365
1426
  const format = getFormatFromDateParts(parts, separator, leadingZero, fullYear);
1366
1427
  return {
1367
1428
  value: Math.round(delta / MS_PER_DAY),
@@ -1452,7 +1513,7 @@ function parseTime(str) {
1452
1513
  if (hours >= 24) {
1453
1514
  format = "hhhh:mm:ss";
1454
1515
  }
1455
- const jsDate = new Date(1899, 11, 30, hours, minutes, seconds);
1516
+ const jsDate = new DateTime(1899, 11, 30, hours, minutes, seconds);
1456
1517
  return {
1457
1518
  value: hours / 24 + minutes / 1440 + seconds / 86400,
1458
1519
  format: format,
@@ -1466,7 +1527,7 @@ function parseTime(str) {
1466
1527
  // -----------------------------------------------------------------------------
1467
1528
  function numberToJsDate(value) {
1468
1529
  const truncValue = Math.trunc(value);
1469
- let date = new Date(truncValue * MS_PER_DAY - DATE_JS_1900_OFFSET);
1530
+ let date = DateTime.fromTimestamp(truncValue * MS_PER_DAY - DATE_JS_1900_OFFSET);
1470
1531
  let time = value - truncValue;
1471
1532
  time = time < 0 ? 1 + time : time;
1472
1533
  const hours = Math.round(time * 24);
@@ -1486,7 +1547,7 @@ function jsDateToNumber(date) {
1486
1547
  }
1487
1548
  /** Return the number of days in the current month of the given date */
1488
1549
  function getDaysInMonth(date) {
1489
- return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
1550
+ return new DateTime(date.getFullYear(), date.getMonth() + 1, 0).getDate();
1490
1551
  }
1491
1552
  function isLastDayOfMonth(date) {
1492
1553
  return getDaysInMonth(date) === date.getDate();
@@ -1504,7 +1565,7 @@ function addMonthsToDate(date, months, keepEndOfMonth) {
1504
1565
  const yStart = date.getFullYear();
1505
1566
  const mStart = date.getMonth();
1506
1567
  const dStart = date.getDate();
1507
- const jsDate = new Date(yStart, mStart + months);
1568
+ const jsDate = new DateTime(yStart, mStart + months, 1);
1508
1569
  if (keepEndOfMonth && dStart === getDaysInMonth(date)) {
1509
1570
  jsDate.setDate(getDaysInMonth(jsDate));
1510
1571
  }
@@ -2727,20 +2788,20 @@ function flattenRowFirst(items, callback) {
2727
2788
  }
2728
2789
 
2729
2790
  function toCriterionDateNumber(dateValue) {
2730
- const today = new Date();
2791
+ const today = DateTime.now();
2731
2792
  switch (dateValue) {
2732
2793
  case "today":
2733
2794
  return jsDateToNumber(today);
2734
2795
  case "yesterday":
2735
- return jsDateToNumber(new Date(today.setDate(today.getDate() - 1)));
2796
+ return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() - 1)));
2736
2797
  case "tomorrow":
2737
- return jsDateToNumber(new Date(today.setDate(today.getDate() + 1)));
2798
+ return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() + 1)));
2738
2799
  case "lastWeek":
2739
- return jsDateToNumber(new Date(today.setDate(today.getDate() - 7)));
2800
+ return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() - 7)));
2740
2801
  case "lastMonth":
2741
- return jsDateToNumber(new Date(today.setMonth(today.getMonth() - 1)));
2802
+ return jsDateToNumber(DateTime.fromTimestamp(today.setMonth(today.getMonth() - 1)));
2742
2803
  case "lastYear":
2743
- return jsDateToNumber(new Date(today.setFullYear(today.getFullYear() - 1)));
2804
+ return jsDateToNumber(DateTime.fromTimestamp(today.setFullYear(today.getFullYear() - 1)));
2744
2805
  }
2745
2806
  }
2746
2807
  /** Get all the dates values of a criterion converted to numbers, converting date values such as "today" to actual dates */
@@ -3098,7 +3159,7 @@ function formatJSTime(jsDate, format) {
3098
3159
  .map((p) => {
3099
3160
  switch (p) {
3100
3161
  case "hhhh":
3101
- const helapsedHours = Math.floor((jsDate.getTime() - INITIAL_1900_DAY) / (60 * 60 * 1000));
3162
+ const helapsedHours = Math.floor((jsDate.getTime() - INITIAL_1900_DAY.getTime()) / (60 * 60 * 1000));
3102
3163
  return helapsedHours.toString();
3103
3164
  case "hh":
3104
3165
  return hours.toString().padStart(2, "0");
@@ -5288,7 +5349,7 @@ function createScorecardChartRuntime(chart, getters) {
5288
5349
  };
5289
5350
  baselineCell = getters.getEvaluatedCell(baselinePosition);
5290
5351
  }
5291
- const background = getters.getBackgroundOfSingleCellChart(chart.background, chart.keyValue);
5352
+ const { background, fontColor } = getters.getStyleOfSingleCellChart(chart.background, chart.keyValue);
5292
5353
  const locale = getters.getLocale();
5293
5354
  return {
5294
5355
  title: _t(chart.title),
@@ -5297,7 +5358,7 @@ function createScorecardChartRuntime(chart, getters) {
5297
5358
  baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, chart.baselineMode),
5298
5359
  baselineColor: getBaselineColor(baselineCell, chart.baselineMode, keyValueCell, chart.baselineColorUp, chart.baselineColorDown),
5299
5360
  baselineDescr: chart.baselineDescr ? _t(chart.baselineDescr) : "",
5300
- fontColor: chartFontColor(background),
5361
+ fontColor,
5301
5362
  background,
5302
5363
  baselineStyle: chart.baselineMode !== "percentage" && baseline
5303
5364
  ? getters.getCellStyle({
@@ -6592,6 +6653,7 @@ const FilterMenuPopoverBuilder = {
6592
6653
  },
6593
6654
  };
6594
6655
 
6656
+ const macRegex = /Mac/i;
6595
6657
  /**
6596
6658
  * Return true if the event was triggered from
6597
6659
  * a child element.
@@ -6631,7 +6693,15 @@ function getOpenedMenus() {
6631
6693
  return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
6632
6694
  }
6633
6695
  function isMacOS() {
6634
- return navigator.userAgent.toUpperCase().indexOf("MAC") >= 0;
6696
+ return Boolean(macRegex.test(navigator.userAgent));
6697
+ }
6698
+ /**
6699
+ * @param {KeyboardEvent | MouseEvent} ev
6700
+ * @returns Returns true if the event was triggered with the "ctrl" modifier pressed.
6701
+ * On Mac, this is the "meta" or "command" key.
6702
+ */
6703
+ function isCtrlKey(ev) {
6704
+ return isMacOS() ? ev.metaKey : ev.ctrlKey;
6635
6705
  }
6636
6706
 
6637
6707
  /**
@@ -8880,7 +8950,7 @@ function createGaugeChartRuntime(chart, getters) {
8880
8950
  });
8881
8951
  return {
8882
8952
  chartJsConfig: config,
8883
- background: getters.getBackgroundOfSingleCellChart(chart.background, dataRange),
8953
+ background: getters.getStyleOfSingleCellChart(chart.background, dataRange).background,
8884
8954
  };
8885
8955
  }
8886
8956
 
@@ -8961,7 +9031,7 @@ function getFormatMinDisplayUnit(format) {
8961
9031
  else if (format.includes("h") || format.includes("H")) {
8962
9032
  return "hour";
8963
9033
  }
8964
- else if (format.includes("D")) {
9034
+ else if (format.includes("d")) {
8965
9035
  return "day";
8966
9036
  }
8967
9037
  else if (format.includes("M")) {
@@ -10909,6 +10979,9 @@ function arg(definition, description = "") {
10909
10979
  function makeArg(str, description) {
10910
10980
  let parts = str.match(ARG_REGEXP);
10911
10981
  let name = parts[1].trim();
10982
+ if (!name) {
10983
+ throw new Error(`Function argument definition is missing a name: '${str}'.`);
10984
+ }
10912
10985
  let types = [];
10913
10986
  let isOptional = false;
10914
10987
  let isRepeating = false;
@@ -14614,7 +14687,7 @@ const DATE = {
14614
14687
  if (_year < 1900) {
14615
14688
  _year += 1900;
14616
14689
  }
14617
- const jsDate = new Date(_year, _month - 1, _day);
14690
+ const jsDate = new DateTime(_year, _month - 1, _day);
14618
14691
  const result = jsDateToRoundNumber(jsDate);
14619
14692
  assert(() => result >= 0, _t("The function [[FUNCTION_NAME]] result must be greater than or equal 01/01/1900."));
14620
14693
  return result;
@@ -14657,7 +14730,7 @@ const DATEDIF = {
14657
14730
  // See: https://support.microsoft.com/en-us/office/datedif-function-25dba1a4-2812-480b-84dd-8b32a451b35c
14658
14731
  let days = jsEndDate.getDate() - jsStartDate.getDate();
14659
14732
  if (days < 0) {
14660
- const monthBeforeEndMonth = new Date(jsEndDate.getFullYear(), jsEndDate.getMonth() - 1, 1);
14733
+ const monthBeforeEndMonth = new DateTime(jsEndDate.getFullYear(), jsEndDate.getMonth() - 1, 1);
14661
14734
  const daysInMonthBeforeEndMonth = getDaysInMonth(monthBeforeEndMonth);
14662
14735
  days = daysInMonthBeforeEndMonth - Math.abs(days);
14663
14736
  }
@@ -14666,7 +14739,7 @@ const DATEDIF = {
14666
14739
  if (areTwoDatesWithinOneYear(_startDate, _endDate)) {
14667
14740
  return getTimeDifferenceInWholeDays(jsStartDate, jsEndDate);
14668
14741
  }
14669
- const endDateWithinOneYear = new Date(jsStartDate.getFullYear(), jsEndDate.getMonth(), jsEndDate.getDate());
14742
+ const endDateWithinOneYear = new DateTime(jsStartDate.getFullYear(), jsEndDate.getMonth(), jsEndDate.getDate());
14670
14743
  let days = getTimeDifferenceInWholeDays(jsStartDate, endDateWithinOneYear);
14671
14744
  if (days < 0) {
14672
14745
  endDateWithinOneYear.setFullYear(jsStartDate.getFullYear() + 1);
@@ -14783,7 +14856,7 @@ const EOMONTH = {
14783
14856
  const _months = Math.trunc(toNumber(months, this.locale));
14784
14857
  const yStart = _startDate.getFullYear();
14785
14858
  const mStart = _startDate.getMonth();
14786
- const jsDate = new Date(yStart, mStart + _months + 1, 0);
14859
+ const jsDate = new DateTime(yStart, mStart + _months + 1, 0);
14787
14860
  return jsDateToRoundNumber(jsDate);
14788
14861
  },
14789
14862
  isExported: true,
@@ -14820,17 +14893,17 @@ const ISOWEEKNUM = {
14820
14893
  // The first week of the year is the week that contains the first
14821
14894
  // Thursday of the year.
14822
14895
  let firstThursday = 1;
14823
- while (new Date(y, 0, firstThursday).getDay() !== 4) {
14896
+ while (new DateTime(y, 0, firstThursday).getDay() !== 4) {
14824
14897
  firstThursday += 1;
14825
14898
  }
14826
- const firstDayOfFirstWeek = new Date(y, 0, firstThursday - 3);
14899
+ const firstDayOfFirstWeek = new DateTime(y, 0, firstThursday - 3);
14827
14900
  // The last week of the year is the week that contains the last Thursday of
14828
14901
  // the year.
14829
14902
  let lastThursday = 31;
14830
- while (new Date(y, 11, lastThursday).getDay() !== 4) {
14903
+ while (new DateTime(y, 11, lastThursday).getDay() !== 4) {
14831
14904
  lastThursday -= 1;
14832
14905
  }
14833
- const lastDayOfLastWeek = new Date(y, 11, lastThursday + 3);
14906
+ const lastDayOfLastWeek = new DateTime(y, 11, lastThursday + 3);
14834
14907
  // B - If our date > lastDayOfLastWeek then it's in the weeks of the year after
14835
14908
  // If our date < firstDayOfFirstWeek then it's in the weeks of the year before
14836
14909
  let offsetYear;
@@ -14856,17 +14929,17 @@ const ISOWEEKNUM = {
14856
14929
  case 1:
14857
14930
  // firstDay is the 1st day of the 1st week of the year after
14858
14931
  // firstDay = lastDayOfLastWeek + 1 Day
14859
- firstDay = new Date(y, 11, lastThursday + 3 + 1);
14932
+ firstDay = new DateTime(y, 11, lastThursday + 3 + 1);
14860
14933
  break;
14861
14934
  case -1:
14862
14935
  // firstDay is the 1st day of the 1st week of the previous year.
14863
14936
  // The first week of the previous year is the week that contains the
14864
14937
  // first Thursday of the previous year.
14865
14938
  let firstThursdayPreviousYear = 1;
14866
- while (new Date(y - 1, 0, firstThursdayPreviousYear).getDay() !== 4) {
14939
+ while (new DateTime(y - 1, 0, firstThursdayPreviousYear).getDay() !== 4) {
14867
14940
  firstThursdayPreviousYear += 1;
14868
14941
  }
14869
- firstDay = new Date(y - 1, 0, firstThursdayPreviousYear - 3);
14942
+ firstDay = new DateTime(y - 1, 0, firstThursdayPreviousYear - 3);
14870
14943
  break;
14871
14944
  }
14872
14945
  const diff = (_date.getTime() - firstDay.getTime()) / MS_PER_DAY;
@@ -15001,8 +15074,8 @@ const NETWORKDAYS_INTL = {
15001
15074
  });
15002
15075
  }
15003
15076
  const invertDate = _startDate.getTime() > _endDate.getTime();
15004
- const stopDate = new Date((invertDate ? _startDate : _endDate).getTime());
15005
- let stepDate = new Date((invertDate ? _endDate : _startDate).getTime());
15077
+ const stopDate = DateTime.fromTimestamp((invertDate ? _startDate : _endDate).getTime());
15078
+ let stepDate = DateTime.fromTimestamp((invertDate ? _endDate : _startDate).getTime());
15006
15079
  const timeStopDate = stopDate.getTime();
15007
15080
  let timeStepDate = stepDate.getTime();
15008
15081
  let netWorkingDay = 0;
@@ -15028,8 +15101,7 @@ const NOW = {
15028
15101
  return getDateTimeFormat(this.locale);
15029
15102
  },
15030
15103
  compute: function () {
15031
- let today = new Date();
15032
- today.setMilliseconds(0);
15104
+ let today = DateTime.now();
15033
15105
  const delta = today.getTime() - INITIAL_1900_DAY.getTime();
15034
15106
  const time = today.getHours() / 24 + today.getMinutes() / 1440 + today.getSeconds() / 86400;
15035
15107
  return Math.floor(delta / MS_PER_DAY) + time;
@@ -15103,8 +15175,8 @@ const TODAY = {
15103
15175
  return this.locale.dateFormat;
15104
15176
  },
15105
15177
  compute: function () {
15106
- const today = new Date();
15107
- const jsDate = new Date(today.getFullYear(), today.getMonth(), today.getDate());
15178
+ const today = DateTime.now();
15179
+ const jsDate = new DateTime(today.getFullYear(), today.getMonth(), today.getDate());
15108
15180
  return jsDateToRoundNumber(jsDate);
15109
15181
  },
15110
15182
  isExported: true,
@@ -15159,10 +15231,10 @@ const WEEKNUM = {
15159
15231
  }
15160
15232
  const y = _date.getFullYear();
15161
15233
  let dayStart = 1;
15162
- let startDayOfFirstWeek = new Date(y, 0, dayStart);
15234
+ let startDayOfFirstWeek = new DateTime(y, 0, dayStart);
15163
15235
  while (startDayOfFirstWeek.getDay() !== startDayOfWeek) {
15164
15236
  dayStart += 1;
15165
- startDayOfFirstWeek = new Date(y, 0, dayStart);
15237
+ startDayOfFirstWeek = new DateTime(y, 0, dayStart);
15166
15238
  }
15167
15239
  const dif = (_date.getTime() - startDayOfFirstWeek.getTime()) / MS_PER_DAY;
15168
15240
  if (dif < 0) {
@@ -15220,7 +15292,7 @@ const WORKDAY_INTL = {
15220
15292
  timesHoliday.add(holiday.getTime());
15221
15293
  });
15222
15294
  }
15223
- let stepDate = new Date(_startDate.getTime());
15295
+ let stepDate = DateTime.fromTimestamp(_startDate.getTime());
15224
15296
  let timeStepDate = stepDate.getTime();
15225
15297
  const unitDay = Math.sign(_numDays);
15226
15298
  let stepDay = Math.abs(_numDays);
@@ -15284,7 +15356,7 @@ const MONTH_START = {
15284
15356
  const _startDate = toJsDate(date, this.locale);
15285
15357
  const yStart = _startDate.getFullYear();
15286
15358
  const mStart = _startDate.getMonth();
15287
- const jsDate = new Date(yStart, mStart, 1);
15359
+ const jsDate = new DateTime(yStart, mStart, 1);
15288
15360
  return jsDateToRoundNumber(jsDate);
15289
15361
  },
15290
15362
  };
@@ -15326,7 +15398,7 @@ const QUARTER_START = {
15326
15398
  compute: function (date) {
15327
15399
  const quarter = QUARTER.compute.bind(this)(date);
15328
15400
  const year = YEAR.compute.bind(this)(date);
15329
- const jsDate = new Date(year, (quarter - 1) * 3, 1);
15401
+ const jsDate = new DateTime(year, (quarter - 1) * 3, 1);
15330
15402
  return jsDateToRoundNumber(jsDate);
15331
15403
  },
15332
15404
  };
@@ -15343,7 +15415,7 @@ const QUARTER_END = {
15343
15415
  compute: function (date) {
15344
15416
  const quarter = QUARTER.compute.bind(this)(date);
15345
15417
  const year = YEAR.compute.bind(this)(date);
15346
- const jsDate = new Date(year, quarter * 3, 0);
15418
+ const jsDate = new DateTime(year, quarter * 3, 0);
15347
15419
  return jsDateToRoundNumber(jsDate);
15348
15420
  },
15349
15421
  };
@@ -15359,7 +15431,7 @@ const YEAR_START = {
15359
15431
  },
15360
15432
  compute: function (date) {
15361
15433
  const year = YEAR.compute.bind(this)(date);
15362
- const jsDate = new Date(year, 0, 1);
15434
+ const jsDate = new DateTime(year, 0, 1);
15363
15435
  return jsDateToRoundNumber(jsDate);
15364
15436
  },
15365
15437
  };
@@ -15375,7 +15447,7 @@ const YEAR_END = {
15375
15447
  },
15376
15448
  compute: function (date) {
15377
15449
  const year = YEAR.compute.bind(this)(date);
15378
- const jsDate = new Date(year + 1, 0, 0);
15450
+ const jsDate = new DateTime(year + 1, 0, 0);
15379
15451
  return jsDateToRoundNumber(jsDate);
15380
15452
  },
15381
15453
  };
@@ -15423,8 +15495,8 @@ const DEFAULT_DELTA_ARG = 0;
15423
15495
  const DELTA = {
15424
15496
  description: _t("Compare two numeric values, returning 1 if they're equal."),
15425
15497
  args: [
15426
- arg(" (number)", _t("The first number to compare.")),
15427
- arg(` (number, default=${DEFAULT_DELTA_ARG})`, _t("The second number to compare.")),
15498
+ arg("number1 (number)", _t("The first number to compare.")),
15499
+ arg(`number2 (number, default=${DEFAULT_DELTA_ARG})`, _t("The second number to compare.")),
15428
15500
  ],
15429
15501
  returns: ["NUMBER"],
15430
15502
  compute: function (number1, number2 = DEFAULT_DELTA_ARG) {
@@ -15851,7 +15923,7 @@ function assertDeprecationFactorStrictlyPositive(factor) {
15851
15923
  function assertSettlementLessThanOneYearBeforeMaturity(settlement, maturity, locale) {
15852
15924
  const startDate = toJsDate(settlement, locale);
15853
15925
  const endDate = toJsDate(maturity, locale);
15854
- const startDatePlusOneYear = new Date(startDate);
15926
+ const startDatePlusOneYear = toJsDate(settlement, locale);
15855
15927
  startDatePlusOneYear.setFullYear(startDate.getFullYear() + 1);
15856
15928
  assert(() => endDate.getTime() <= startDatePlusOneYear.getTime(), _t("The settlement date (%s) must at most one year after the maturity date (%s).", settlement.toString(), maturity.toString()));
15857
15929
  }
@@ -15986,7 +16058,7 @@ const AMORLINC = {
15986
16058
  arg("salvage (number)", _t("The value of the asset at the end of depreciation.")),
15987
16059
  arg("period (number)", _t("The single period within life for which to calculate depreciation.")),
15988
16060
  arg("rate (number)", _t("The deprecation rate.")),
15989
- arg(" (number, optional)", _t("An indicator of what day count method to use.")),
16061
+ arg("day_count_convention (number, optional)", _t("An indicator of what day count method to use.")),
15990
16062
  ],
15991
16063
  returns: ["NUMBER"],
15992
16064
  compute: function (cost, purchaseDate, firstPeriodEnd, salvage, period, rate, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
@@ -19010,8 +19082,8 @@ const MID = {
19010
19082
  description: _t("A segment of a string."),
19011
19083
  args: [
19012
19084
  arg("text (string)", _t("The string to extract a segment from.")),
19013
- arg(" (number)", _t("The index from the left of string from which to begin extracting. The first character in string has the index 1.")),
19014
- arg(" (number)", _t("The length of the segment to extract.")),
19085
+ arg("starting_at (number)", _t("The index from the left of string from which to begin extracting. The first character in string has the index 1.")),
19086
+ arg("extract_length (number)", _t("The length of the segment to extract.")),
19015
19087
  ],
19016
19088
  returns: ["STRING"],
19017
19089
  compute: function (text, starting_at, extract_length) {
@@ -21214,10 +21286,10 @@ const arrowMap = {
21214
21286
  function updateSelectionWithArrowKeys(ev, selection) {
21215
21287
  const direction = arrowMap[ev.key];
21216
21288
  if (ev.shiftKey) {
21217
- selection.resizeAnchorZone(direction, ev.ctrlKey ? "end" : 1);
21289
+ selection.resizeAnchorZone(direction, isCtrlKey(ev) ? "end" : 1);
21218
21290
  }
21219
21291
  else {
21220
- selection.moveAnchorCell(direction, ev.ctrlKey ? "end" : 1);
21292
+ selection.moveAnchorCell(direction, isCtrlKey(ev) ? "end" : 1);
21221
21293
  }
21222
21294
  }
21223
21295
 
@@ -24205,7 +24277,7 @@ dataValidationEvaluatorRegistry.add("dateIs", {
24205
24277
  return false;
24206
24278
  }
24207
24279
  if (["lastWeek", "lastMonth", "lastYear"].includes(criterion.dateValue)) {
24208
- const today = jsDateToRoundNumber(new Date());
24280
+ const today = jsDateToRoundNumber(DateTime.now());
24209
24281
  return isDateBetween(dateValue, today, criterionValue);
24210
24282
  }
24211
24283
  return areDatesSameDay(dateValue, criterionValue);
@@ -28202,7 +28274,10 @@ class GridOverlay extends owl.Component {
28202
28274
  return;
28203
28275
  }
28204
28276
  const [col, row] = this.getCartesianCoordinates(ev);
28205
- this.props.onCellClicked(col, row, { shiftKey: ev.shiftKey, ctrlKey: ev.ctrlKey });
28277
+ this.props.onCellClicked(col, row, {
28278
+ expandZone: ev.shiftKey,
28279
+ addZone: isCtrlKey(ev),
28280
+ });
28206
28281
  }
28207
28282
  onDoubleClick(ev) {
28208
28283
  const [col, row] = this.getCartesianCoordinates(ev);
@@ -28432,7 +28507,7 @@ class AbstractResizer extends owl.Component {
28432
28507
  this._increaseSelection(index);
28433
28508
  }
28434
28509
  else {
28435
- this._selectElement(index, ev.ctrlKey);
28510
+ this._selectElement(index, isCtrlKey(ev));
28436
28511
  }
28437
28512
  this.lastSelectedElementIndex = index;
28438
28513
  const mouseMoveSelect = (col, row) => {
@@ -28596,8 +28671,8 @@ class ColResizer extends AbstractResizer {
28596
28671
  this.env.raiseError(MergeErrorMessage);
28597
28672
  }
28598
28673
  }
28599
- _selectElement(index, ctrlKey) {
28600
- this.env.model.selection.selectColumn(index, ctrlKey ? "newAnchor" : "overrideSelection");
28674
+ _selectElement(index, addDistinctHeader) {
28675
+ this.env.model.selection.selectColumn(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
28601
28676
  }
28602
28677
  _increaseSelection(index) {
28603
28678
  this.env.model.selection.selectColumn(index, "updateAnchor");
@@ -28770,8 +28845,8 @@ class RowResizer extends AbstractResizer {
28770
28845
  this.env.raiseError(MergeErrorMessage);
28771
28846
  }
28772
28847
  }
28773
- _selectElement(index, ctrlKey) {
28774
- this.env.model.selection.selectRow(index, ctrlKey ? "newAnchor" : "overrideSelection");
28848
+ _selectElement(index, addDistinctHeader) {
28849
+ this.env.model.selection.selectRow(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
28775
28850
  }
28776
28851
  _increaseSelection(index) {
28777
28852
  this.env.model.selection.selectRow(index, "updateAnchor");
@@ -29573,17 +29648,17 @@ class Grid extends owl.Component {
29573
29648
  // ---------------------------------------------------------------------------
29574
29649
  // Zone selection with mouse
29575
29650
  // ---------------------------------------------------------------------------
29576
- onCellClicked(col, row, { ctrlKey, shiftKey }) {
29651
+ onCellClicked(col, row, { addZone, expandZone }) {
29577
29652
  if (this.env.model.getters.hasOpenedPopover()) {
29578
29653
  this.closeOpenedPopover();
29579
29654
  }
29580
29655
  if (this.env.model.getters.getEditionMode() === "editing") {
29581
29656
  interactiveStopEdition(this.env);
29582
29657
  }
29583
- if (shiftKey) {
29658
+ if (expandZone) {
29584
29659
  this.env.model.selection.setAnchorCorner(col, row);
29585
29660
  }
29586
- else if (ctrlKey) {
29661
+ else if (addZone) {
29587
29662
  this.env.model.selection.addCellToSelection(col, row);
29588
29663
  }
29589
29664
  else {
@@ -29644,7 +29719,7 @@ class Grid extends owl.Component {
29644
29719
  onKeydown(ev) {
29645
29720
  const eventKey = ev.key.toUpperCase();
29646
29721
  let keyDownString = "";
29647
- if (ev.ctrlKey && eventKey !== "CTRL")
29722
+ if (isCtrlKey(ev) && eventKey !== "CTRL")
29648
29723
  keyDownString += "CTRL+";
29649
29724
  if (ev.metaKey)
29650
29725
  keyDownString += "CTRL+";
@@ -34535,7 +34610,7 @@ class BordersPlugin extends CorePlugin {
34535
34610
  */
34536
34611
  function getBorderId(border) {
34537
34612
  for (let [key, value] of Object.entries(borders)) {
34538
- if (stringify(value) === stringify(border)) {
34613
+ if (deepEquals(value, border)) {
34539
34614
  return parseInt(key, 10);
34540
34615
  }
34541
34616
  }
@@ -39854,56 +39929,758 @@ class CompilationParametersBuilder {
39854
39929
  }
39855
39930
  }
39856
39931
 
39932
+ function quickselect(arr, k, left, right, compare) {
39933
+ quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);
39934
+ }
39935
+
39936
+ function quickselectStep(arr, k, left, right, compare) {
39937
+
39938
+ while (right > left) {
39939
+ if (right - left > 600) {
39940
+ var n = right - left + 1;
39941
+ var m = k - left + 1;
39942
+ var z = Math.log(n);
39943
+ var s = 0.5 * Math.exp(2 * z / 3);
39944
+ var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
39945
+ var newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
39946
+ var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
39947
+ quickselectStep(arr, k, newLeft, newRight, compare);
39948
+ }
39949
+
39950
+ var t = arr[k];
39951
+ var i = left;
39952
+ var j = right;
39953
+
39954
+ swap(arr, left, k);
39955
+ if (compare(arr[right], t) > 0) swap(arr, left, right);
39956
+
39957
+ while (i < j) {
39958
+ swap(arr, i, j);
39959
+ i++;
39960
+ j--;
39961
+ while (compare(arr[i], t) < 0) i++;
39962
+ while (compare(arr[j], t) > 0) j--;
39963
+ }
39964
+
39965
+ if (compare(arr[left], t) === 0) swap(arr, left, j);
39966
+ else {
39967
+ j++;
39968
+ swap(arr, j, right);
39969
+ }
39970
+
39971
+ if (j <= k) left = j + 1;
39972
+ if (k <= j) right = j - 1;
39973
+ }
39974
+ }
39975
+
39976
+ function swap(arr, i, j) {
39977
+ var tmp = arr[i];
39978
+ arr[i] = arr[j];
39979
+ arr[j] = tmp;
39980
+ }
39981
+
39982
+ function defaultCompare(a, b) {
39983
+ return a < b ? -1 : a > b ? 1 : 0;
39984
+ }
39985
+
39986
+ class RBush {
39987
+ constructor(maxEntries = 9) {
39988
+ // max entries in a node is 9 by default; min node fill is 40% for best performance
39989
+ this._maxEntries = Math.max(4, maxEntries);
39990
+ this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
39991
+ this.clear();
39992
+ }
39993
+
39994
+ all() {
39995
+ return this._all(this.data, []);
39996
+ }
39997
+
39998
+ search(bbox) {
39999
+ let node = this.data;
40000
+ const result = [];
40001
+
40002
+ if (!intersects(bbox, node)) return result;
40003
+
40004
+ const toBBox = this.toBBox;
40005
+ const nodesToSearch = [];
40006
+
40007
+ while (node) {
40008
+ for (let i = 0; i < node.children.length; i++) {
40009
+ const child = node.children[i];
40010
+ const childBBox = node.leaf ? toBBox(child) : child;
40011
+
40012
+ if (intersects(bbox, childBBox)) {
40013
+ if (node.leaf) result.push(child);
40014
+ else if (contains(bbox, childBBox)) this._all(child, result);
40015
+ else nodesToSearch.push(child);
40016
+ }
40017
+ }
40018
+ node = nodesToSearch.pop();
40019
+ }
40020
+
40021
+ return result;
40022
+ }
40023
+
40024
+ collides(bbox) {
40025
+ let node = this.data;
40026
+
40027
+ if (!intersects(bbox, node)) return false;
40028
+
40029
+ const nodesToSearch = [];
40030
+ while (node) {
40031
+ for (let i = 0; i < node.children.length; i++) {
40032
+ const child = node.children[i];
40033
+ const childBBox = node.leaf ? this.toBBox(child) : child;
40034
+
40035
+ if (intersects(bbox, childBBox)) {
40036
+ if (node.leaf || contains(bbox, childBBox)) return true;
40037
+ nodesToSearch.push(child);
40038
+ }
40039
+ }
40040
+ node = nodesToSearch.pop();
40041
+ }
40042
+
40043
+ return false;
40044
+ }
40045
+
40046
+ load(data) {
40047
+ if (!(data && data.length)) return this;
40048
+
40049
+ if (data.length < this._minEntries) {
40050
+ for (let i = 0; i < data.length; i++) {
40051
+ this.insert(data[i]);
40052
+ }
40053
+ return this;
40054
+ }
40055
+
40056
+ // recursively build the tree with the given data from scratch using OMT algorithm
40057
+ let node = this._build(data.slice(), 0, data.length - 1, 0);
40058
+
40059
+ if (!this.data.children.length) {
40060
+ // save as is if tree is empty
40061
+ this.data = node;
40062
+
40063
+ } else if (this.data.height === node.height) {
40064
+ // split root if trees have the same height
40065
+ this._splitRoot(this.data, node);
40066
+
40067
+ } else {
40068
+ if (this.data.height < node.height) {
40069
+ // swap trees if inserted one is bigger
40070
+ const tmpNode = this.data;
40071
+ this.data = node;
40072
+ node = tmpNode;
40073
+ }
40074
+
40075
+ // insert the small tree into the large tree at appropriate level
40076
+ this._insert(node, this.data.height - node.height - 1, true);
40077
+ }
40078
+
40079
+ return this;
40080
+ }
40081
+
40082
+ insert(item) {
40083
+ if (item) this._insert(item, this.data.height - 1);
40084
+ return this;
40085
+ }
40086
+
40087
+ clear() {
40088
+ this.data = createNode([]);
40089
+ return this;
40090
+ }
40091
+
40092
+ remove(item, equalsFn) {
40093
+ if (!item) return this;
40094
+
40095
+ let node = this.data;
40096
+ const bbox = this.toBBox(item);
40097
+ const path = [];
40098
+ const indexes = [];
40099
+ let i, parent, goingUp;
40100
+
40101
+ // depth-first iterative tree traversal
40102
+ while (node || path.length) {
40103
+
40104
+ if (!node) { // go up
40105
+ node = path.pop();
40106
+ parent = path[path.length - 1];
40107
+ i = indexes.pop();
40108
+ goingUp = true;
40109
+ }
40110
+
40111
+ if (node.leaf) { // check current node
40112
+ const index = findItem(item, node.children, equalsFn);
40113
+
40114
+ if (index !== -1) {
40115
+ // item found, remove the item and condense tree upwards
40116
+ node.children.splice(index, 1);
40117
+ path.push(node);
40118
+ this._condense(path);
40119
+ return this;
40120
+ }
40121
+ }
40122
+
40123
+ if (!goingUp && !node.leaf && contains(node, bbox)) { // go down
40124
+ path.push(node);
40125
+ indexes.push(i);
40126
+ i = 0;
40127
+ parent = node;
40128
+ node = node.children[0];
40129
+
40130
+ } else if (parent) { // go right
40131
+ i++;
40132
+ node = parent.children[i];
40133
+ goingUp = false;
40134
+
40135
+ } else node = null; // nothing found
40136
+ }
40137
+
40138
+ return this;
40139
+ }
40140
+
40141
+ toBBox(item) { return item; }
40142
+
40143
+ compareMinX(a, b) { return a.minX - b.minX; }
40144
+ compareMinY(a, b) { return a.minY - b.minY; }
40145
+
40146
+ toJSON() { return this.data; }
40147
+
40148
+ fromJSON(data) {
40149
+ this.data = data;
40150
+ return this;
40151
+ }
40152
+
40153
+ _all(node, result) {
40154
+ const nodesToSearch = [];
40155
+ while (node) {
40156
+ if (node.leaf) result.push(...node.children);
40157
+ else nodesToSearch.push(...node.children);
40158
+
40159
+ node = nodesToSearch.pop();
40160
+ }
40161
+ return result;
40162
+ }
40163
+
40164
+ _build(items, left, right, height) {
40165
+
40166
+ const N = right - left + 1;
40167
+ let M = this._maxEntries;
40168
+ let node;
40169
+
40170
+ if (N <= M) {
40171
+ // reached leaf level; return leaf
40172
+ node = createNode(items.slice(left, right + 1));
40173
+ calcBBox(node, this.toBBox);
40174
+ return node;
40175
+ }
40176
+
40177
+ if (!height) {
40178
+ // target height of the bulk-loaded tree
40179
+ height = Math.ceil(Math.log(N) / Math.log(M));
40180
+
40181
+ // target number of root entries to maximize storage utilization
40182
+ M = Math.ceil(N / Math.pow(M, height - 1));
40183
+ }
40184
+
40185
+ node = createNode([]);
40186
+ node.leaf = false;
40187
+ node.height = height;
40188
+
40189
+ // split the items into M mostly square tiles
40190
+
40191
+ const N2 = Math.ceil(N / M);
40192
+ const N1 = N2 * Math.ceil(Math.sqrt(M));
40193
+
40194
+ multiSelect(items, left, right, N1, this.compareMinX);
40195
+
40196
+ for (let i = left; i <= right; i += N1) {
40197
+
40198
+ const right2 = Math.min(i + N1 - 1, right);
40199
+
40200
+ multiSelect(items, i, right2, N2, this.compareMinY);
40201
+
40202
+ for (let j = i; j <= right2; j += N2) {
40203
+
40204
+ const right3 = Math.min(j + N2 - 1, right2);
40205
+
40206
+ // pack each entry recursively
40207
+ node.children.push(this._build(items, j, right3, height - 1));
40208
+ }
40209
+ }
40210
+
40211
+ calcBBox(node, this.toBBox);
40212
+
40213
+ return node;
40214
+ }
40215
+
40216
+ _chooseSubtree(bbox, node, level, path) {
40217
+ while (true) {
40218
+ path.push(node);
40219
+
40220
+ if (node.leaf || path.length - 1 === level) break;
40221
+
40222
+ let minArea = Infinity;
40223
+ let minEnlargement = Infinity;
40224
+ let targetNode;
40225
+
40226
+ for (let i = 0; i < node.children.length; i++) {
40227
+ const child = node.children[i];
40228
+ const area = bboxArea(child);
40229
+ const enlargement = enlargedArea(bbox, child) - area;
40230
+
40231
+ // choose entry with the least area enlargement
40232
+ if (enlargement < minEnlargement) {
40233
+ minEnlargement = enlargement;
40234
+ minArea = area < minArea ? area : minArea;
40235
+ targetNode = child;
40236
+
40237
+ } else if (enlargement === minEnlargement) {
40238
+ // otherwise choose one with the smallest area
40239
+ if (area < minArea) {
40240
+ minArea = area;
40241
+ targetNode = child;
40242
+ }
40243
+ }
40244
+ }
40245
+
40246
+ node = targetNode || node.children[0];
40247
+ }
40248
+
40249
+ return node;
40250
+ }
40251
+
40252
+ _insert(item, level, isNode) {
40253
+ const bbox = isNode ? item : this.toBBox(item);
40254
+ const insertPath = [];
40255
+
40256
+ // find the best node for accommodating the item, saving all nodes along the path too
40257
+ const node = this._chooseSubtree(bbox, this.data, level, insertPath);
40258
+
40259
+ // put the item into the node
40260
+ node.children.push(item);
40261
+ extend(node, bbox);
40262
+
40263
+ // split on node overflow; propagate upwards if necessary
40264
+ while (level >= 0) {
40265
+ if (insertPath[level].children.length > this._maxEntries) {
40266
+ this._split(insertPath, level);
40267
+ level--;
40268
+ } else break;
40269
+ }
40270
+
40271
+ // adjust bboxes along the insertion path
40272
+ this._adjustParentBBoxes(bbox, insertPath, level);
40273
+ }
40274
+
40275
+ // split overflowed node into two
40276
+ _split(insertPath, level) {
40277
+ const node = insertPath[level];
40278
+ const M = node.children.length;
40279
+ const m = this._minEntries;
40280
+
40281
+ this._chooseSplitAxis(node, m, M);
40282
+
40283
+ const splitIndex = this._chooseSplitIndex(node, m, M);
40284
+
40285
+ const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
40286
+ newNode.height = node.height;
40287
+ newNode.leaf = node.leaf;
40288
+
40289
+ calcBBox(node, this.toBBox);
40290
+ calcBBox(newNode, this.toBBox);
40291
+
40292
+ if (level) insertPath[level - 1].children.push(newNode);
40293
+ else this._splitRoot(node, newNode);
40294
+ }
40295
+
40296
+ _splitRoot(node, newNode) {
40297
+ // split root node
40298
+ this.data = createNode([node, newNode]);
40299
+ this.data.height = node.height + 1;
40300
+ this.data.leaf = false;
40301
+ calcBBox(this.data, this.toBBox);
40302
+ }
40303
+
40304
+ _chooseSplitIndex(node, m, M) {
40305
+ let index;
40306
+ let minOverlap = Infinity;
40307
+ let minArea = Infinity;
40308
+
40309
+ for (let i = m; i <= M - m; i++) {
40310
+ const bbox1 = distBBox(node, 0, i, this.toBBox);
40311
+ const bbox2 = distBBox(node, i, M, this.toBBox);
40312
+
40313
+ const overlap = intersectionArea(bbox1, bbox2);
40314
+ const area = bboxArea(bbox1) + bboxArea(bbox2);
40315
+
40316
+ // choose distribution with minimum overlap
40317
+ if (overlap < minOverlap) {
40318
+ minOverlap = overlap;
40319
+ index = i;
40320
+
40321
+ minArea = area < minArea ? area : minArea;
40322
+
40323
+ } else if (overlap === minOverlap) {
40324
+ // otherwise choose distribution with minimum area
40325
+ if (area < minArea) {
40326
+ minArea = area;
40327
+ index = i;
40328
+ }
40329
+ }
40330
+ }
40331
+
40332
+ return index || M - m;
40333
+ }
40334
+
40335
+ // sorts node children by the best axis for split
40336
+ _chooseSplitAxis(node, m, M) {
40337
+ const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;
40338
+ const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;
40339
+ const xMargin = this._allDistMargin(node, m, M, compareMinX);
40340
+ const yMargin = this._allDistMargin(node, m, M, compareMinY);
40341
+
40342
+ // if total distributions margin value is minimal for x, sort by minX,
40343
+ // otherwise it's already sorted by minY
40344
+ if (xMargin < yMargin) node.children.sort(compareMinX);
40345
+ }
40346
+
40347
+ // total margin of all possible split distributions where each node is at least m full
40348
+ _allDistMargin(node, m, M, compare) {
40349
+ node.children.sort(compare);
40350
+
40351
+ const toBBox = this.toBBox;
40352
+ const leftBBox = distBBox(node, 0, m, toBBox);
40353
+ const rightBBox = distBBox(node, M - m, M, toBBox);
40354
+ let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);
40355
+
40356
+ for (let i = m; i < M - m; i++) {
40357
+ const child = node.children[i];
40358
+ extend(leftBBox, node.leaf ? toBBox(child) : child);
40359
+ margin += bboxMargin(leftBBox);
40360
+ }
40361
+
40362
+ for (let i = M - m - 1; i >= m; i--) {
40363
+ const child = node.children[i];
40364
+ extend(rightBBox, node.leaf ? toBBox(child) : child);
40365
+ margin += bboxMargin(rightBBox);
40366
+ }
40367
+
40368
+ return margin;
40369
+ }
40370
+
40371
+ _adjustParentBBoxes(bbox, path, level) {
40372
+ // adjust bboxes along the given tree path
40373
+ for (let i = level; i >= 0; i--) {
40374
+ extend(path[i], bbox);
40375
+ }
40376
+ }
40377
+
40378
+ _condense(path) {
40379
+ // go through the path, removing empty nodes and updating bboxes
40380
+ for (let i = path.length - 1, siblings; i >= 0; i--) {
40381
+ if (path[i].children.length === 0) {
40382
+ if (i > 0) {
40383
+ siblings = path[i - 1].children;
40384
+ siblings.splice(siblings.indexOf(path[i]), 1);
40385
+
40386
+ } else this.clear();
40387
+
40388
+ } else calcBBox(path[i], this.toBBox);
40389
+ }
40390
+ }
40391
+ }
40392
+
40393
+ function findItem(item, items, equalsFn) {
40394
+ if (!equalsFn) return items.indexOf(item);
40395
+
40396
+ for (let i = 0; i < items.length; i++) {
40397
+ if (equalsFn(item, items[i])) return i;
40398
+ }
40399
+ return -1;
40400
+ }
40401
+
40402
+ // calculate node's bbox from bboxes of its children
40403
+ function calcBBox(node, toBBox) {
40404
+ distBBox(node, 0, node.children.length, toBBox, node);
40405
+ }
40406
+
40407
+ // min bounding rectangle of node children from k to p-1
40408
+ function distBBox(node, k, p, toBBox, destNode) {
40409
+ if (!destNode) destNode = createNode(null);
40410
+ destNode.minX = Infinity;
40411
+ destNode.minY = Infinity;
40412
+ destNode.maxX = -Infinity;
40413
+ destNode.maxY = -Infinity;
40414
+
40415
+ for (let i = k; i < p; i++) {
40416
+ const child = node.children[i];
40417
+ extend(destNode, node.leaf ? toBBox(child) : child);
40418
+ }
40419
+
40420
+ return destNode;
40421
+ }
40422
+
40423
+ function extend(a, b) {
40424
+ a.minX = Math.min(a.minX, b.minX);
40425
+ a.minY = Math.min(a.minY, b.minY);
40426
+ a.maxX = Math.max(a.maxX, b.maxX);
40427
+ a.maxY = Math.max(a.maxY, b.maxY);
40428
+ return a;
40429
+ }
40430
+
40431
+ function compareNodeMinX(a, b) { return a.minX - b.minX; }
40432
+ function compareNodeMinY(a, b) { return a.minY - b.minY; }
40433
+
40434
+ function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); }
40435
+ function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }
40436
+
40437
+ function enlargedArea(a, b) {
40438
+ return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *
40439
+ (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
40440
+ }
40441
+
40442
+ function intersectionArea(a, b) {
40443
+ const minX = Math.max(a.minX, b.minX);
40444
+ const minY = Math.max(a.minY, b.minY);
40445
+ const maxX = Math.min(a.maxX, b.maxX);
40446
+ const maxY = Math.min(a.maxY, b.maxY);
40447
+
40448
+ return Math.max(0, maxX - minX) *
40449
+ Math.max(0, maxY - minY);
40450
+ }
40451
+
40452
+ function contains(a, b) {
40453
+ return a.minX <= b.minX &&
40454
+ a.minY <= b.minY &&
40455
+ b.maxX <= a.maxX &&
40456
+ b.maxY <= a.maxY;
40457
+ }
40458
+
40459
+ function intersects(a, b) {
40460
+ return b.minX <= a.maxX &&
40461
+ b.minY <= a.maxY &&
40462
+ b.maxX >= a.minX &&
40463
+ b.maxY >= a.minY;
40464
+ }
40465
+
40466
+ function createNode(children) {
40467
+ return {
40468
+ children,
40469
+ height: 1,
40470
+ leaf: true,
40471
+ minX: Infinity,
40472
+ minY: Infinity,
40473
+ maxX: -Infinity,
40474
+ maxY: -Infinity
40475
+ };
40476
+ }
40477
+
40478
+ // sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
40479
+ // combines selection algorithm with binary divide & conquer approach
40480
+
40481
+ function multiSelect(arr, left, right, n, compare) {
40482
+ const stack = [left, right];
40483
+
40484
+ while (stack.length) {
40485
+ right = stack.pop();
40486
+ left = stack.pop();
40487
+
40488
+ if (right - left <= n) continue;
40489
+
40490
+ const mid = left + Math.ceil((right - left) / n / 2) * n;
40491
+ quickselect(arr, mid, left, right, compare);
40492
+
40493
+ stack.push(left, mid, mid, right);
40494
+ }
40495
+ }
40496
+
39857
40497
  /**
39858
- * This class is an implementation of a dependency Graph.
40498
+ * R-Tree Data Structure
40499
+ *
40500
+ * R-Tree is a spatial data structure used for efficient indexing and querying
40501
+ * of multi-dimensional objects, particularly in geometric and spatial applications.
40502
+ *
40503
+ * It organizes objects into a tree hierarchy, grouping nearby objects together
40504
+ * in bounding boxes. Each node in the tree represents a bounding box that
40505
+ * contains its child nodes or leaf objects. This hierarchical structure allows
40506
+ * for faster spatial queries.
40507
+ *
40508
+ * @see https://en.wikipedia.org/wiki/R-tree
40509
+ *
40510
+ * Consider a 2D Space with four zones: A, B, C, D
40511
+ * +--------------------------+
40512
+ * | |
40513
+ * | +---+ +-------+ |
40514
+ * | | A | | B | |
40515
+ * | +---+ +-------+ |
40516
+ * | |
40517
+ * | |
40518
+ * | +---+ |
40519
+ * | | C | |
40520
+ * | +---+ |
40521
+ * | +-----------+ |
40522
+ * | | D | |
40523
+ * | +-----------+ |
40524
+ * | |
40525
+ * +--------------------------+
40526
+ *
40527
+ * It groups together zones that are spatially close into a minimum bounding box.
40528
+ * For example, A and B are grouped together in rectangle R1, and C and D are grouped
40529
+ * in R2.
40530
+ *
40531
+ * R0
40532
+ * +--------------------------+
40533
+ * | R1 |
40534
+ * | +-----------------+ |
40535
+ * | | A | | B | |
40536
+ * | +-----------------+ |
40537
+ * | |
40538
+ * | R2 |
40539
+ * | +---+---+---+ |
40540
+ * | | | C | | |
40541
+ * | | +---+ | |
40542
+ * | +-----------+ |
40543
+ * | | D | |
40544
+ * | +-----------+ |
40545
+ * | |
40546
+ * +--------------------------+
40547
+ *
40548
+ * The tree would look like this:
40549
+ * R0
40550
+ * / \
40551
+ * / \
40552
+ * R1 R2
40553
+ * | |
40554
+ * A,B C,D
40555
+
40556
+ * Choosing how to group the zones is crucial for the performance of the tree.
40557
+ * Key considerations include avoiding excessive empty space coverage and minimizing overlap
40558
+ * to reduce the number of subtrees processed during searches.
40559
+ *
40560
+ * Various heuristics exist for determining the optimal grouping strategy, such as "least enlargement"
40561
+ * which prioritizes grouping nodes resulting in the smallest increase in bounding box size. In cases where
40562
+ * the choice cannot be made based on this criterion due to the same enlargement for different groupings,
40563
+ * we then evaluate "least area," aiming to minimize the overall area of bounding boxes.
40564
+ *
40565
+ * This implementation is tailored for spreadsheet use, indexing objects associated
40566
+ * with a zone and a sheet.
40567
+ *
40568
+ * It uses the RBush library under the hood. One 2D RBush R-tree per sheet.
40569
+ * @see https://github.com/mourner/rbush
40570
+ */
40571
+ class SpreadsheetRTree {
40572
+ /**
40573
+ * One 2D R-tree per sheet
40574
+ */
40575
+ rTrees = {};
40576
+ /**
40577
+ * Bulk-inserts the given items into the tree. Bulk insertion is usually ~2-3 times
40578
+ * faster than inserting items one by one. After bulk loading (bulk insertion into
40579
+ * an empty tree), subsequent query performance is also ~20-30% better.
40580
+ */
40581
+ constructor(items = []) {
40582
+ const rangesPerSheet = {};
40583
+ for (const item of items) {
40584
+ const sheetId = item.boundingBox.sheetId;
40585
+ if (!rangesPerSheet[sheetId]) {
40586
+ rangesPerSheet[sheetId] = [];
40587
+ }
40588
+ rangesPerSheet[sheetId].push(item);
40589
+ }
40590
+ for (const sheetId in rangesPerSheet) {
40591
+ this.rTrees[sheetId] = new ZoneRBush();
40592
+ this.rTrees[sheetId].load(rangesPerSheet[sheetId]); // bulk-insert
40593
+ }
40594
+ }
40595
+ insert(item) {
40596
+ const sheetId = item.boundingBox.sheetId;
40597
+ if (!this.rTrees[sheetId]) {
40598
+ this.rTrees[sheetId] = new ZoneRBush();
40599
+ }
40600
+ this.rTrees[sheetId].insert(item);
40601
+ }
40602
+ search({ zone, sheetId }) {
40603
+ if (!this.rTrees[sheetId]) {
40604
+ return [];
40605
+ }
40606
+ return this.rTrees[sheetId].search({
40607
+ minX: zone.left,
40608
+ minY: zone.top,
40609
+ maxX: zone.right,
40610
+ maxY: zone.bottom,
40611
+ });
40612
+ }
40613
+ remove(item) {
40614
+ const sheetId = item.boundingBox.sheetId;
40615
+ if (!this.rTrees[sheetId]) {
40616
+ return;
40617
+ }
40618
+ this.rTrees[sheetId].remove(item, deepEquals);
40619
+ }
40620
+ }
40621
+ /**
40622
+ * RBush extension to use zones as bounding boxes
40623
+ */
40624
+ class ZoneRBush extends RBush {
40625
+ toBBox({ boundingBox }) {
40626
+ const zone = boundingBox.zone;
40627
+ return {
40628
+ minX: zone.left,
40629
+ minY: zone.top,
40630
+ maxX: zone.right,
40631
+ maxY: zone.bottom,
40632
+ };
40633
+ }
40634
+ compareMinX(a, b) {
40635
+ return a.boundingBox.zone.left - b.boundingBox.zone.left;
40636
+ }
40637
+ compareMinY(a, b) {
40638
+ return a.boundingBox.zone.top - b.boundingBox.zone.top;
40639
+ }
40640
+ }
40641
+
40642
+ /**
40643
+ * Implementation of a dependency Graph.
39859
40644
  * The graph is used to evaluate the cells in the correct
39860
40645
  * order, and should be updated each time a cell's content is modified
39861
40646
  *
40647
+ * It uses an R-Tree data structure to efficiently find dependent cells.
39862
40648
  */
39863
40649
  class FormulaDependencyGraph {
39864
- /**
39865
- * Internal structure:
39866
- * - key: a cell position (encoded as an integer)
39867
- * - value: a set of cell positions that depends on the key
39868
- *
39869
- * Given
39870
- * - A1:"= B1 + SQRT(B2)"
39871
- * - C1:"= B1";
39872
- * - C2:"= C1"
39873
- *
39874
- * we will have something like:
39875
- * - B1 ---> (A1, C1) meaning A1 and C1 depends on B1
39876
- * - B2 ---> (A1) meaning A1 depends on B2
39877
- * - C1 ---> (C2) meaning C2 depends on C1
39878
- */
39879
- inverseDependencies = new Map();
40650
+ encoder;
39880
40651
  dependencies = new Map();
40652
+ rTree;
40653
+ constructor(encoder, data = []) {
40654
+ this.encoder = encoder;
40655
+ this.rTree = new SpreadsheetRTree(data);
40656
+ }
39881
40657
  removeAllDependencies(formulaPositionId) {
39882
- const dependencies = this.dependencies.get(formulaPositionId);
39883
- if (!dependencies) {
40658
+ const ranges = this.dependencies.get(formulaPositionId);
40659
+ if (!ranges) {
39884
40660
  return;
39885
40661
  }
39886
- for (const dependency of dependencies) {
39887
- this.inverseDependencies.get(dependency)?.delete(formulaPositionId);
40662
+ for (const range of ranges) {
40663
+ this.rTree.remove(range);
39888
40664
  }
39889
40665
  this.dependencies.delete(formulaPositionId);
39890
40666
  }
39891
40667
  addDependencies(formulaPositionId, dependencies) {
39892
- for (const dependency of dependencies) {
39893
- const inverseDependencies = this.inverseDependencies.get(dependency);
39894
- if (inverseDependencies) {
39895
- inverseDependencies.add(formulaPositionId);
39896
- }
39897
- else {
39898
- this.inverseDependencies.set(dependency, new Set([formulaPositionId]));
39899
- }
40668
+ const rTreeItems = dependencies.map(({ sheetId, zone }) => ({
40669
+ data: formulaPositionId,
40670
+ boundingBox: {
40671
+ zone,
40672
+ sheetId,
40673
+ },
40674
+ }));
40675
+ for (const item of rTreeItems) {
40676
+ this.rTree.insert(item);
39900
40677
  }
39901
40678
  const existingDependencies = this.dependencies.get(formulaPositionId);
39902
40679
  if (existingDependencies) {
39903
- existingDependencies.push(...dependencies);
40680
+ existingDependencies.push(...rTreeItems);
39904
40681
  }
39905
40682
  else {
39906
- this.dependencies.set(formulaPositionId, dependencies);
40683
+ this.dependencies.set(formulaPositionId, rTreeItems);
39907
40684
  }
39908
40685
  }
39909
40686
  /**
@@ -39911,20 +40688,20 @@ class FormulaDependencyGraph {
39911
40688
  * in the correct order they should be evaluated.
39912
40689
  * This is called a topological ordering (excluding cycles)
39913
40690
  */
39914
- getCellsDependingOn(positionIds) {
40691
+ getCellsDependingOn(ranges) {
39915
40692
  const visited = new JetSet();
39916
- const queue = Array.from(positionIds).reverse();
40693
+ const queue = Array.from(ranges).reverse();
39917
40694
  while (queue.length > 0) {
39918
- const node = queue.pop();
39919
- visited.add(node);
39920
- const adjacentNodes = this.inverseDependencies.get(node) || new Set();
39921
- for (const adjacentNode of adjacentNodes) {
39922
- if (!visited.has(adjacentNode)) {
39923
- queue.push(adjacentNode);
40695
+ const range = queue.pop();
40696
+ visited.add(...this.encoder.encodeBoundingBox(range));
40697
+ const impactedPositionIds = this.rTree.search(range).map((dep) => dep.data);
40698
+ for (const positionId of impactedPositionIds) {
40699
+ if (!visited.has(positionId)) {
40700
+ queue.push(this.encoder.decodeToBoundingBox(positionId));
39924
40701
  }
39925
40702
  }
39926
40703
  }
39927
- visited.delete(...positionIds);
40704
+ visited.delete(...ranges.flatMap((r) => this.encoder.encodeBoundingBox(r)));
39928
40705
  return visited;
39929
40706
  }
39930
40707
  }
@@ -40012,9 +40789,9 @@ class Evaluator {
40012
40789
  context;
40013
40790
  getters;
40014
40791
  compilationParams;
40015
- positionEncoder = new PositionBitsEncoder();
40792
+ encoder = new PositionBitsEncoder();
40016
40793
  evaluatedCells = new Map();
40017
- formulaDependencies = lazy(new FormulaDependencyGraph());
40794
+ formulaDependencies = lazy(new FormulaDependencyGraph(this.encoder));
40018
40795
  blockedArrayFormulas = new Set();
40019
40796
  spreadingRelations = new SpreadingRelation();
40020
40797
  constructor(context, getters) {
@@ -40023,23 +40800,23 @@ class Evaluator {
40023
40800
  this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
40024
40801
  }
40025
40802
  getEvaluatedCell(position) {
40026
- return (this.evaluatedCells.get(this.encodePosition(position)) ||
40803
+ return (this.evaluatedCells.get(this.encoder.encode(position)) ||
40027
40804
  createEvaluatedCell("", { locale: this.getters.getLocale() }));
40028
40805
  }
40029
40806
  getSpreadPositionsOf(position) {
40030
- const positionId = this.encodePosition(position);
40807
+ const positionId = this.encoder.encode(position);
40031
40808
  if (!this.spreadingRelations.isArrayFormula(positionId)) {
40032
40809
  return [];
40033
40810
  }
40034
- return Array.from(this.spreadingRelations.getArrayResultPositionIds(positionId)).map(this.decodePosition.bind(this));
40811
+ return Array.from(this.spreadingRelations.getArrayResultPositionIds(positionId)).map((positionId) => this.encoder.decode(positionId));
40035
40812
  }
40036
40813
  getArrayFormulaSpreadingOn(position) {
40037
- const positionId = this.encodePosition(position);
40814
+ const positionId = this.encoder.encode(position);
40038
40815
  const formulaPosition = this.getArrayFormulaSpreadingOnId(positionId);
40039
- return formulaPosition !== undefined ? this.decodePosition(formulaPosition) : undefined;
40816
+ return formulaPosition !== undefined ? this.encoder.decode(formulaPosition) : undefined;
40040
40817
  }
40041
40818
  getEvaluatedPositions() {
40042
- return [...this.evaluatedCells.keys()].map(this.decodePosition.bind(this));
40819
+ return [...this.evaluatedCells.keys()].map((p) => this.encoder.decode(p));
40043
40820
  }
40044
40821
  getArrayFormulaSpreadingOnId(positionId) {
40045
40822
  if (!this.spreadingRelations.hasArrayFormulaResult(positionId)) {
@@ -40049,7 +40826,7 @@ class Evaluator {
40049
40826
  return Array.from(arrayFormulas).find((positionId) => !this.blockedArrayFormulas.has(positionId));
40050
40827
  }
40051
40828
  updateDependencies(position) {
40052
- const positionId = this.encodePosition(position);
40829
+ const positionId = this.encoder.encode(position);
40053
40830
  this.formulaDependencies().removeAllDependencies(positionId);
40054
40831
  const dependencies = this.getDirectDependencies(positionId);
40055
40832
  this.formulaDependencies().addDependencies(positionId, dependencies);
@@ -40059,12 +40836,12 @@ class Evaluator {
40059
40836
  this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
40060
40837
  }
40061
40838
  evaluateCells(positions) {
40062
- const cells = positions.map(this.encodePosition.bind(this));
40839
+ const cells = positions.map((p) => this.encoder.encode(p));
40063
40840
  const cellsToCompute = new JetSet(cells);
40064
- const arrayFormulas = this.getArrayFormulasImpactedByChangesOf(cells);
40841
+ const arrayFormulasPositionIds = this.getArrayFormulasImpactedByChangesOf(cells);
40065
40842
  cellsToCompute.add(...this.getCellsDependingOn(cells));
40066
- cellsToCompute.add(...arrayFormulas);
40067
- cellsToCompute.add(...this.getCellsDependingOn(arrayFormulas));
40843
+ cellsToCompute.add(...arrayFormulasPositionIds);
40844
+ cellsToCompute.add(...this.getCellsDependingOn(arrayFormulasPositionIds));
40068
40845
  this.evaluate(cellsToCompute);
40069
40846
  }
40070
40847
  getArrayFormulasImpactedByChangesOf(positionIds) {
@@ -40087,12 +40864,14 @@ class Evaluator {
40087
40864
  this.blockedArrayFormulas = new Set();
40088
40865
  this.spreadingRelations = new SpreadingRelation();
40089
40866
  this.formulaDependencies = lazy(() => {
40090
- const dependencyGraph = new FormulaDependencyGraph();
40091
- for (const positionId of this.getAllCells()) {
40092
- const dependencies = this.getDirectDependencies(positionId);
40093
- dependencyGraph.addDependencies(positionId, dependencies);
40094
- }
40095
- return dependencyGraph;
40867
+ const dependencies = [...this.getAllCells()].flatMap((positionId) => this.getDirectDependencies(positionId).map((range) => ({
40868
+ data: positionId,
40869
+ boundingBox: {
40870
+ zone: range.zone,
40871
+ sheetId: range.sheetId,
40872
+ },
40873
+ })));
40874
+ return new FormulaDependencyGraph(this.encoder, dependencies);
40096
40875
  });
40097
40876
  }
40098
40877
  evaluateAllCells() {
@@ -40114,7 +40893,7 @@ class Evaluator {
40114
40893
  for (const sheetId of this.getters.getSheetIds()) {
40115
40894
  const cellIds = this.getters.getCells(sheetId);
40116
40895
  for (const cellId in cellIds) {
40117
- positionIds.add(this.encodePosition(this.getters.getCellPosition(cellId)));
40896
+ positionIds.add(this.encoder.encode(this.getters.getCellPosition(cellId)));
40118
40897
  }
40119
40898
  }
40120
40899
  return positionIds;
@@ -40181,7 +40960,7 @@ class Evaluator {
40181
40960
  }
40182
40961
  }
40183
40962
  computeAndSave(position) {
40184
- const positionId = this.encodePosition(position);
40963
+ const positionId = this.encoder.encode(position);
40185
40964
  const evaluatedCell = this.computeCell(positionId);
40186
40965
  if (!this.evaluatedCells.has(positionId)) {
40187
40966
  this.setEvaluatedCell(positionId, evaluatedCell);
@@ -40241,15 +41020,15 @@ class Evaluator {
40241
41020
  throw new Error(_t("Result couldn't be automatically expanded. Please insert more columns and rows."));
40242
41021
  }
40243
41022
  updateSpreadRelation({ sheetId, col, row, }) {
40244
- const arrayFormulaPositionId = this.encodePosition({ sheetId, col, row });
41023
+ const arrayFormulaPositionId = this.encoder.encode({ sheetId, col, row });
40245
41024
  return (i, j) => {
40246
41025
  const position = { sheetId, col: i + col, row: j + row };
40247
- const resultPositionId = this.encodePosition(position);
41026
+ const resultPositionId = this.encoder.encode(position);
40248
41027
  this.spreadingRelations.addRelation({ resultPositionId, arrayFormulaPositionId });
40249
41028
  };
40250
41029
  }
40251
41030
  checkCollision({ sheetId, col, row }) {
40252
- const formulaPositionId = this.encodePosition({ sheetId, col, row });
41031
+ const formulaPositionId = this.encoder.encode({ sheetId, col, row });
40253
41032
  return (i, j) => {
40254
41033
  const position = { sheetId: sheetId, col: i + col, row: j + row };
40255
41034
  const rawCell = this.getters.getCell(position);
@@ -40270,7 +41049,7 @@ class Evaluator {
40270
41049
  format: format || matrixResult[i][j]?.format,
40271
41050
  locale: this.getters.getLocale(),
40272
41051
  });
40273
- const positionId = this.encodePosition(position);
41052
+ const positionId = this.encoder.encode(position);
40274
41053
  this.setEvaluatedCell(positionId, evaluatedCell);
40275
41054
  // check if formula dependencies present in the spread zone
40276
41055
  // if so, they need to be recomputed
@@ -40302,29 +41081,17 @@ class Evaluator {
40302
41081
  if (!cell?.isFormula) {
40303
41082
  return [];
40304
41083
  }
40305
- const dependencies = [];
40306
- for (const range of cell.compiledFormula.dependencies) {
40307
- if (range.invalidSheetName || range.invalidXc) {
40308
- continue;
40309
- }
40310
- const sheetId = range.sheetId;
40311
- forEachPositionsInZone(range.zone, (col, row) => {
40312
- dependencies.push(this.encodePosition({ sheetId, col, row }));
40313
- });
40314
- }
40315
- return dependencies;
41084
+ return cell.compiledFormula.dependencies;
40316
41085
  }
40317
41086
  getCellsDependingOn(positionIds) {
40318
- return this.formulaDependencies().getCellsDependingOn(positionIds);
41087
+ const ranges = [];
41088
+ for (const positionId of positionIds) {
41089
+ ranges.push(this.encoder.decodeToBoundingBox(positionId));
41090
+ }
41091
+ return this.formulaDependencies().getCellsDependingOn(ranges);
40319
41092
  }
40320
41093
  getCell(positionId) {
40321
- return this.getters.getCell(this.decodePosition(positionId));
40322
- }
40323
- encodePosition(position) {
40324
- return this.positionEncoder.encode(position);
40325
- }
40326
- decodePosition(positionId) {
40327
- return this.positionEncoder.decode(positionId);
41094
+ return this.getters.getCell(this.encoder.decode(positionId));
40328
41095
  }
40329
41096
  }
40330
41097
  function forEachSpreadPositionInMatrix(nbColumns, nbRows, callback) {
@@ -40380,6 +41147,13 @@ class PositionBitsEncoder {
40380
41147
  encode({ sheetId, col, row }) {
40381
41148
  return (this.encodeSheet(sheetId) << 42n) | (BigInt(col) << 21n) | BigInt(row);
40382
41149
  }
41150
+ encodeBoundingBox({ sheetId, zone }) {
41151
+ const positions = [];
41152
+ forEachPositionsInZone(zone, (col, row) => {
41153
+ positions.push(this.encode({ sheetId, col, row }));
41154
+ });
41155
+ return positions;
41156
+ }
40383
41157
  decode(id) {
40384
41158
  // keep only the last 21 bits by AND-ing the bit sequence with 21 ones
40385
41159
  const row = Number(id & 2097151n);
@@ -40387,6 +41161,10 @@ class PositionBitsEncoder {
40387
41161
  const sheetId = this.decodeSheet(id >> 42n);
40388
41162
  return { sheetId, col, row };
40389
41163
  }
41164
+ decodeToBoundingBox(id) {
41165
+ const { sheetId, col, row } = this.decode(id);
41166
+ return { sheetId, zone: { left: col, top: row, right: col, bottom: row } };
41167
+ }
40390
41168
  encodeSheet(sheetId) {
40391
41169
  const sheetKey = this.sheetMapping[sheetId];
40392
41170
  if (sheetKey === undefined) {
@@ -40557,7 +41335,12 @@ class EvaluationPlugin extends UIPlugin {
40557
41335
  // Getters
40558
41336
  // ---------------------------------------------------------------------------
40559
41337
  evaluateFormula(sheetId, formulaString) {
40560
- return this.evaluator.evaluateFormula(sheetId, formulaString);
41338
+ try {
41339
+ return this.evaluator.evaluateFormula(sheetId, formulaString);
41340
+ }
41341
+ catch (error) {
41342
+ return error instanceof EvaluationError ? error.errorType : CellErrorType.GenericError;
41343
+ }
40561
41344
  }
40562
41345
  /**
40563
41346
  * Return the value of each cell in the range as they are displayed in the grid.
@@ -40826,7 +41609,7 @@ class CustomColorsPlugin extends UIPlugin {
40826
41609
  }
40827
41610
 
40828
41611
  class EvaluationChartPlugin extends UIPlugin {
40829
- static getters = ["getChartRuntime", "getBackgroundOfSingleCellChart"];
41612
+ static getters = ["getChartRuntime", "getStyleOfSingleCellChart"];
40830
41613
  charts = {};
40831
41614
  createRuntimeChart = chartRuntimeFactory(this.getters);
40832
41615
  handle(cmd) {
@@ -40864,25 +41647,26 @@ class EvaluationChartPlugin extends UIPlugin {
40864
41647
  return this.charts[figureId];
40865
41648
  }
40866
41649
  /**
40867
- * Get the background color of a chart based on the color of the first cell of the main range
40868
- * of the chart. In order of priority, it will return :
40869
- *
40870
- * - the chart background color if one is defined
40871
- * - the fill color of the cell if one is defined
40872
- * - the fill color of the cell from conditional formats if one is defined
40873
- * - the default chart color if no other color is defined
41650
+ * Get the background and textColor of a chart based on the color of the first cell of the main range of the chart.
40874
41651
  */
40875
- getBackgroundOfSingleCellChart(chartBackground, mainRange) {
41652
+ getStyleOfSingleCellChart(chartBackground, mainRange) {
40876
41653
  if (chartBackground)
40877
- return chartBackground;
41654
+ return { background: chartBackground, fontColor: chartFontColor(chartBackground) };
40878
41655
  if (!mainRange) {
40879
- return BACKGROUND_CHART_COLOR;
41656
+ return {
41657
+ background: BACKGROUND_CHART_COLOR,
41658
+ fontColor: chartFontColor(BACKGROUND_CHART_COLOR),
41659
+ };
40880
41660
  }
40881
41661
  const col = mainRange.zone.left;
40882
41662
  const row = mainRange.zone.top;
40883
41663
  const sheetId = mainRange.sheetId;
40884
41664
  const style = this.getters.getCellComputedStyle({ sheetId, col, row });
40885
- return style.fillColor || BACKGROUND_CHART_COLOR;
41665
+ const background = style.fillColor || BACKGROUND_CHART_COLOR;
41666
+ return {
41667
+ background,
41668
+ fontColor: style.textColor || chartFontColor(background),
41669
+ };
40886
41670
  }
40887
41671
  exportForExcel(data) {
40888
41672
  for (const sheet of data.sheets) {
@@ -40993,45 +41777,40 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
40993
41777
  getComputedStyles(sheetId) {
40994
41778
  const computedStyle = {};
40995
41779
  for (let cf of this.getters.getConditionalFormats(sheetId).reverse()) {
40996
- try {
40997
- switch (cf.rule.type) {
40998
- case "ColorScaleRule":
40999
- for (let range of cf.ranges) {
41000
- this.applyColorScale(sheetId, range, cf.rule, computedStyle);
41001
- }
41002
- break;
41003
- case "CellIsRule":
41004
- const formulas = cf.rule.values.map((value) => value.startsWith("=") ? compile(value) : undefined);
41005
- for (let ref of cf.ranges) {
41006
- const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
41007
- for (let row = zone.top; row <= zone.bottom; row++) {
41008
- for (let col = zone.left; col <= zone.right; col++) {
41009
- const predicate = this.rulePredicate[cf.rule.type];
41010
- const target = { sheetId, col, row };
41011
- const values = cf.rule.values.map((value, i) => {
41012
- const compiledFormula = formulas[i];
41013
- if (compiledFormula) {
41014
- return this.getters.getTranslatedCellFormula(sheetId, col - zone.left, row - zone.top, {
41015
- ...compiledFormula,
41016
- dependencies: compiledFormula.dependencies.map((d) => this.getters.getRangeFromSheetXC(sheetId, d)),
41017
- });
41018
- }
41019
- return value;
41020
- });
41021
- if (predicate && predicate(target, { ...cf.rule, values })) {
41022
- if (!computedStyle[col])
41023
- computedStyle[col] = [];
41024
- // we must combine all the properties of all the CF rules applied to the given cell
41025
- computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
41780
+ switch (cf.rule.type) {
41781
+ case "ColorScaleRule":
41782
+ for (let range of cf.ranges) {
41783
+ this.applyColorScale(sheetId, range, cf.rule, computedStyle);
41784
+ }
41785
+ break;
41786
+ case "CellIsRule":
41787
+ const formulas = cf.rule.values.map((value) => value.startsWith("=") ? compile(value) : undefined);
41788
+ for (let ref of cf.ranges) {
41789
+ const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
41790
+ for (let row = zone.top; row <= zone.bottom; row++) {
41791
+ for (let col = zone.left; col <= zone.right; col++) {
41792
+ const predicate = this.rulePredicate[cf.rule.type];
41793
+ const target = { sheetId, col, row };
41794
+ const values = cf.rule.values.map((value, i) => {
41795
+ const compiledFormula = formulas[i];
41796
+ if (compiledFormula) {
41797
+ return this.getters.getTranslatedCellFormula(sheetId, col - zone.left, row - zone.top, {
41798
+ ...compiledFormula,
41799
+ dependencies: compiledFormula.dependencies.map((d) => this.getters.getRangeFromSheetXC(sheetId, d)),
41800
+ });
41026
41801
  }
41802
+ return value;
41803
+ });
41804
+ if (predicate && predicate(target, { ...cf.rule, values })) {
41805
+ if (!computedStyle[col])
41806
+ computedStyle[col] = [];
41807
+ // we must combine all the properties of all the CF rules applied to the given cell
41808
+ computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
41027
41809
  }
41028
41810
  }
41029
41811
  }
41030
- break;
41031
- }
41032
- }
41033
- catch (_) {
41034
- // we don't care about the errors within the evaluation of a rule
41812
+ }
41813
+ break;
41035
41814
  }
41036
41815
  }
41037
41816
  return computedStyle;
@@ -41803,7 +42582,7 @@ class AutofillPlugin extends UIPlugin {
41803
42582
  let row = zone.bottom;
41804
42583
  if (col > 0) {
41805
42584
  let leftPosition = { sheetId, col: col - 1, row };
41806
- while (this.getters.getEvaluatedCell(leftPosition).type !== CellValueType.empty ||
42585
+ while (this.getters.getCorrespondingFormulaCell(leftPosition) ||
41807
42586
  this.getters.getCell(leftPosition)?.content) {
41808
42587
  row += 1;
41809
42588
  leftPosition = { sheetId, col: col - 1, row };
@@ -41813,7 +42592,7 @@ class AutofillPlugin extends UIPlugin {
41813
42592
  col = zone.right;
41814
42593
  if (col <= this.getters.getNumberCols(sheetId)) {
41815
42594
  let rightPosition = { sheetId, col: col + 1, row };
41816
- while (this.getters.getEvaluatedCell(rightPosition).type !== CellValueType.empty ||
42595
+ while (this.getters.getCorrespondingFormulaCell(rightPosition) ||
41817
42596
  this.getters.getCell(rightPosition)?.content) {
41818
42597
  row += 1;
41819
42598
  rightPosition = { sheetId, col: col + 1, row };
@@ -52194,7 +52973,7 @@ class Spreadsheet extends owl.Component {
52194
52973
  }
52195
52974
  onKeydown(ev) {
52196
52975
  let keyDownString = "";
52197
- if (ev.ctrlKey || ev.metaKey) {
52976
+ if (isCtrlKey(ev)) {
52198
52977
  keyDownString += "CTRL+";
52199
52978
  }
52200
52979
  keyDownString += ev.key.toUpperCase();
@@ -55906,6 +56685,7 @@ const helpers = {
55906
56685
  colorToRGBA,
55907
56686
  positionToZone,
55908
56687
  isDefined: isDefined$1,
56688
+ isMatrix,
55909
56689
  lazy,
55910
56690
  genericRepeat,
55911
56691
  createAction,
@@ -55989,6 +56769,6 @@ exports.setTranslationMethod = setTranslationMethod;
55989
56769
  exports.tokenize = tokenize;
55990
56770
 
55991
56771
 
55992
- __info__.version = "17.0.5";
55993
- __info__.date = "2023-12-05T09:51:44.657Z";
55994
- __info__.hash = "2480a5d";
56772
+ __info__.version = "17.0.7";
56773
+ __info__.date = "2024-01-12T13:42:04.654Z";
56774
+ __info__.hash = "cadb817";