@odoo/o-spreadsheet 16.4.16 → 16.4.18

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.
@@ -1170,16 +1170,81 @@ function toXC(col, row, rangePart = { colFixed: false, rowFixed: false }) {
1170
1170
  }
1171
1171
 
1172
1172
  // -----------------------------------------------------------------------------
1173
+ /**
1174
+ * A DateTime object that can be used to manipulate spreadsheet dates.
1175
+ * Conceptually, a spreadsheet date is simply a number with a date format,
1176
+ * and it is timezone-agnostic.
1177
+ * This DateTime object consistently uses UTC time to represent a naive date and time.
1178
+ */
1179
+ class DateTime {
1180
+ jsDate;
1181
+ constructor(year, month, day, hours = 0, minutes = 0, seconds = 0) {
1182
+ this.jsDate = new Date(Date.UTC(year, month, day, hours, minutes, seconds, 0));
1183
+ }
1184
+ static fromTimestamp(timestamp) {
1185
+ const date = new Date(timestamp);
1186
+ return new DateTime(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());
1187
+ }
1188
+ static now() {
1189
+ const now = new Date();
1190
+ return new DateTime(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds());
1191
+ }
1192
+ toString() {
1193
+ return this.jsDate.toString();
1194
+ }
1195
+ toLocaleDateString() {
1196
+ return this.jsDate.toLocaleDateString();
1197
+ }
1198
+ getTime() {
1199
+ return this.jsDate.getTime();
1200
+ }
1201
+ getFullYear() {
1202
+ return this.jsDate.getUTCFullYear();
1203
+ }
1204
+ getMonth() {
1205
+ return this.jsDate.getUTCMonth();
1206
+ }
1207
+ getDate() {
1208
+ return this.jsDate.getUTCDate();
1209
+ }
1210
+ getDay() {
1211
+ return this.jsDate.getUTCDay();
1212
+ }
1213
+ getHours() {
1214
+ return this.jsDate.getUTCHours();
1215
+ }
1216
+ getMinutes() {
1217
+ return this.jsDate.getUTCMinutes();
1218
+ }
1219
+ getSeconds() {
1220
+ return this.jsDate.getUTCSeconds();
1221
+ }
1222
+ setFullYear(year) {
1223
+ this.jsDate.setFullYear(year);
1224
+ }
1225
+ setDate(date) {
1226
+ this.jsDate.setUTCDate(date);
1227
+ }
1228
+ setHours(hours) {
1229
+ this.jsDate.setUTCHours(hours);
1230
+ }
1231
+ setMinutes(minutes) {
1232
+ this.jsDate.setUTCMinutes(minutes);
1233
+ }
1234
+ setSeconds(seconds) {
1235
+ this.jsDate.setUTCSeconds(seconds);
1236
+ }
1237
+ }
1173
1238
  // -----------------------------------------------------------------------------
1174
1239
  // Parsing
1175
1240
  // -----------------------------------------------------------------------------
1176
- const INITIAL_1900_DAY = new Date(1899, 11, 30);
1241
+ const INITIAL_1900_DAY = new DateTime(1899, 11, 30);
1177
1242
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
1178
1243
  const CURRENT_MILLENIAL = 2000; // note: don't forget to update this in 2999
1179
- const CURRENT_YEAR = new Date().getFullYear();
1180
- const CURRENT_MONTH = new Date().getMonth();
1181
- const INITIAL_JS_DAY = new Date(0);
1182
- const DATE_JS_1900_OFFSET = INITIAL_JS_DAY - INITIAL_1900_DAY;
1244
+ const CURRENT_YEAR = DateTime.now().getFullYear();
1245
+ const CURRENT_MONTH = DateTime.now().getMonth();
1246
+ const INITIAL_JS_DAY = DateTime.fromTimestamp(0);
1247
+ const DATE_JS_1900_OFFSET = INITIAL_JS_DAY.getTime() - INITIAL_1900_DAY.getTime();
1183
1248
  const mdyDateRegexp = /^\d{1,2}(\/|-|\s)\d{1,2}((\/|-|\s)\d{1,4})?$/;
1184
1249
  const ymdDateRegexp = /^\d{3,4}(\/|-|\s)\d{1,2}(\/|-|\s)\d{1,2}$/;
1185
1250
  const dateSeparatorsRegex = /\/|-|\s/;
@@ -1226,7 +1291,7 @@ function _parseDateTime(str, locale) {
1226
1291
  return {
1227
1292
  value: date.value + time.value,
1228
1293
  format: date.format + " " + (time.format === "hhhh:mm:ss" ? "hh:mm:ss" : time.format),
1229
- 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()),
1294
+ 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()),
1230
1295
  };
1231
1296
  }
1232
1297
  return date || time;
@@ -1298,12 +1363,12 @@ function parseDate(parts, separator) {
1298
1363
  // month + 1: months are 0-indexed in JS
1299
1364
  const leadingZero = (monthStr?.length === 2 && month + 1 < 10) || (dayStr?.length === 2 && day < 10);
1300
1365
  const fullYear = yearStr?.length !== 2;
1301
- const jsDate = new Date(year, month, day);
1366
+ const jsDate = new DateTime(year, month, day);
1302
1367
  if (jsDate.getMonth() !== month || jsDate.getDate() !== day) {
1303
1368
  // invalid date
1304
1369
  return null;
1305
1370
  }
1306
- const delta = jsDate - INITIAL_1900_DAY;
1371
+ const delta = jsDate.getTime() - INITIAL_1900_DAY.getTime();
1307
1372
  const format = getFormatFromDateParts(parts, separator, leadingZero, fullYear);
1308
1373
  return {
1309
1374
  value: Math.round(delta / MS_PER_DAY),
@@ -1394,7 +1459,7 @@ function parseTime(str) {
1394
1459
  if (hours >= 24) {
1395
1460
  format = "hhhh:mm:ss";
1396
1461
  }
1397
- const jsDate = new Date(1899, 11, 30, hours, minutes, seconds);
1462
+ const jsDate = new DateTime(1899, 11, 30, hours, minutes, seconds);
1398
1463
  return {
1399
1464
  value: hours / 24 + minutes / 1440 + seconds / 86400,
1400
1465
  format: format,
@@ -1408,7 +1473,7 @@ function parseTime(str) {
1408
1473
  // -----------------------------------------------------------------------------
1409
1474
  function numberToJsDate(value) {
1410
1475
  const truncValue = Math.trunc(value);
1411
- let date = new Date(truncValue * MS_PER_DAY - DATE_JS_1900_OFFSET);
1476
+ let date = DateTime.fromTimestamp(truncValue * MS_PER_DAY - DATE_JS_1900_OFFSET);
1412
1477
  let time = value - truncValue;
1413
1478
  time = time < 0 ? 1 + time : time;
1414
1479
  const hours = Math.round(time * 24);
@@ -1425,7 +1490,7 @@ function jsDateToRoundNumber(date) {
1425
1490
  }
1426
1491
  /** Return the number of days in the current month of the given date */
1427
1492
  function getDaysInMonth(date) {
1428
- return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
1493
+ return new DateTime(date.getFullYear(), date.getMonth() + 1, 0).getDate();
1429
1494
  }
1430
1495
  function isLastDayOfMonth(date) {
1431
1496
  return getDaysInMonth(date) === date.getDate();
@@ -1443,7 +1508,7 @@ function addMonthsToDate(date, months, keepEndOfMonth) {
1443
1508
  const yStart = date.getFullYear();
1444
1509
  const mStart = date.getMonth();
1445
1510
  const dStart = date.getDate();
1446
- const jsDate = new Date(yStart, mStart + months);
1511
+ const jsDate = new DateTime(yStart, mStart + months, 1);
1447
1512
  if (keepEndOfMonth && dStart === getDaysInMonth(date)) {
1448
1513
  jsDate.setDate(getDaysInMonth(jsDate));
1449
1514
  }
@@ -2047,7 +2112,7 @@ function formatJSTime(jsDate, format) {
2047
2112
  .map((p) => {
2048
2113
  switch (p) {
2049
2114
  case "hhhh":
2050
- const helapsedHours = Math.floor((jsDate.getTime() - INITIAL_1900_DAY) / (60 * 60 * 1000));
2115
+ const helapsedHours = Math.floor((jsDate.getTime() - INITIAL_1900_DAY.getTime()) / (60 * 60 * 1000));
2051
2116
  return helapsedHours.toString();
2052
2117
  case "hh":
2053
2118
  return hours.toString().padStart(2, "0");
@@ -5601,6 +5666,7 @@ const FilterMenuPopoverBuilder = {
5601
5666
  },
5602
5667
  };
5603
5668
 
5669
+ const macRegex = /Mac/i;
5604
5670
  /**
5605
5671
  * Return true if the event was triggered from
5606
5672
  * a child element.
@@ -5640,7 +5706,15 @@ function getOpenedMenus() {
5640
5706
  return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
5641
5707
  }
5642
5708
  function isMacOS() {
5643
- return navigator.userAgent.toUpperCase().indexOf("MAC") >= 0;
5709
+ return Boolean(macRegex.test(navigator.userAgent));
5710
+ }
5711
+ /**
5712
+ * @param {KeyboardEvent | MouseEvent} ev
5713
+ * @returns Returns true if the event was triggered with the "ctrl" modifier pressed.
5714
+ * On Mac, this is the "meta" or "command" key.
5715
+ */
5716
+ function isCtrlKey(ev) {
5717
+ return isMacOS() ? ev.metaKey : ev.ctrlKey;
5644
5718
  }
5645
5719
 
5646
5720
  /**
@@ -5980,6 +6054,10 @@ css /* scss */ `
5980
6054
  justify-content: space-between;
5981
6055
  }
5982
6056
 
6057
+ .o-menu-item-name {
6058
+ min-width: 40%;
6059
+ }
6060
+
5983
6061
  .o-menu-item-icon {
5984
6062
  display: inline-block;
5985
6063
  margin: 0px 8px 0px 0px;
@@ -6314,6 +6392,9 @@ function arg(definition, description = "") {
6314
6392
  function makeArg(str, description) {
6315
6393
  let parts = str.match(ARG_REGEXP);
6316
6394
  let name = parts[1].trim();
6395
+ if (!name) {
6396
+ throw new Error(`Function argument definition is missing a name: '${str}'.`);
6397
+ }
6317
6398
  let types = [];
6318
6399
  let isOptional = false;
6319
6400
  let isRepeating = false;
@@ -9564,7 +9645,7 @@ const DATE = {
9564
9645
  if (_year < 1900) {
9565
9646
  _year += 1900;
9566
9647
  }
9567
- const jsDate = new Date(_year, _month - 1, _day);
9648
+ const jsDate = new DateTime(_year, _month - 1, _day);
9568
9649
  const result = jsDateToRoundNumber(jsDate);
9569
9650
  assert(() => result >= 0, _lt(`The function [[FUNCTION_NAME]] result must be greater than or equal 01/01/1900.`));
9570
9651
  return result;
@@ -9607,7 +9688,7 @@ const DATEDIF = {
9607
9688
  // See: https://support.microsoft.com/en-us/office/datedif-function-25dba1a4-2812-480b-84dd-8b32a451b35c
9608
9689
  let days = jsEndDate.getDate() - jsStartDate.getDate();
9609
9690
  if (days < 0) {
9610
- const monthBeforeEndMonth = new Date(jsEndDate.getFullYear(), jsEndDate.getMonth() - 1, 1);
9691
+ const monthBeforeEndMonth = new DateTime(jsEndDate.getFullYear(), jsEndDate.getMonth() - 1, 1);
9611
9692
  const daysInMonthBeforeEndMonth = getDaysInMonth(monthBeforeEndMonth);
9612
9693
  days = daysInMonthBeforeEndMonth - Math.abs(days);
9613
9694
  }
@@ -9616,7 +9697,7 @@ const DATEDIF = {
9616
9697
  if (areTwoDatesWithinOneYear(_startDate, _endDate)) {
9617
9698
  return getTimeDifferenceInWholeDays(jsStartDate, jsEndDate);
9618
9699
  }
9619
- const endDateWithinOneYear = new Date(jsStartDate.getFullYear(), jsEndDate.getMonth(), jsEndDate.getDate());
9700
+ const endDateWithinOneYear = new DateTime(jsStartDate.getFullYear(), jsEndDate.getMonth(), jsEndDate.getDate());
9620
9701
  let days = getTimeDifferenceInWholeDays(jsStartDate, endDateWithinOneYear);
9621
9702
  if (days < 0) {
9622
9703
  endDateWithinOneYear.setFullYear(jsStartDate.getFullYear() + 1);
@@ -9733,7 +9814,7 @@ const EOMONTH = {
9733
9814
  const _months = Math.trunc(toNumber(months, this.locale));
9734
9815
  const yStart = _startDate.getFullYear();
9735
9816
  const mStart = _startDate.getMonth();
9736
- const jsDate = new Date(yStart, mStart + _months + 1, 0);
9817
+ const jsDate = new DateTime(yStart, mStart + _months + 1, 0);
9737
9818
  return jsDateToRoundNumber(jsDate);
9738
9819
  },
9739
9820
  isExported: true,
@@ -9770,17 +9851,17 @@ const ISOWEEKNUM = {
9770
9851
  // The first week of the year is the week that contains the first
9771
9852
  // Thursday of the year.
9772
9853
  let firstThursday = 1;
9773
- while (new Date(y, 0, firstThursday).getDay() !== 4) {
9854
+ while (new DateTime(y, 0, firstThursday).getDay() !== 4) {
9774
9855
  firstThursday += 1;
9775
9856
  }
9776
- const firstDayOfFirstWeek = new Date(y, 0, firstThursday - 3);
9857
+ const firstDayOfFirstWeek = new DateTime(y, 0, firstThursday - 3);
9777
9858
  // The last week of the year is the week that contains the last Thursday of
9778
9859
  // the year.
9779
9860
  let lastThursday = 31;
9780
- while (new Date(y, 11, lastThursday).getDay() !== 4) {
9861
+ while (new DateTime(y, 11, lastThursday).getDay() !== 4) {
9781
9862
  lastThursday -= 1;
9782
9863
  }
9783
- const lastDayOfLastWeek = new Date(y, 11, lastThursday + 3);
9864
+ const lastDayOfLastWeek = new DateTime(y, 11, lastThursday + 3);
9784
9865
  // B - If our date > lastDayOfLastWeek then it's in the weeks of the year after
9785
9866
  // If our date < firstDayOfFirstWeek then it's in the weeks of the year before
9786
9867
  let offsetYear;
@@ -9806,17 +9887,17 @@ const ISOWEEKNUM = {
9806
9887
  case 1:
9807
9888
  // firstDay is the 1st day of the 1st week of the year after
9808
9889
  // firstDay = lastDayOfLastWeek + 1 Day
9809
- firstDay = new Date(y, 11, lastThursday + 3 + 1);
9890
+ firstDay = new DateTime(y, 11, lastThursday + 3 + 1);
9810
9891
  break;
9811
9892
  case -1:
9812
9893
  // firstDay is the 1st day of the 1st week of the previous year.
9813
9894
  // The first week of the previous year is the week that contains the
9814
9895
  // first Thursday of the previous year.
9815
9896
  let firstThursdayPreviousYear = 1;
9816
- while (new Date(y - 1, 0, firstThursdayPreviousYear).getDay() !== 4) {
9897
+ while (new DateTime(y - 1, 0, firstThursdayPreviousYear).getDay() !== 4) {
9817
9898
  firstThursdayPreviousYear += 1;
9818
9899
  }
9819
- firstDay = new Date(y - 1, 0, firstThursdayPreviousYear - 3);
9900
+ firstDay = new DateTime(y - 1, 0, firstThursdayPreviousYear - 3);
9820
9901
  break;
9821
9902
  }
9822
9903
  const diff = (_date.getTime() - firstDay.getTime()) / MS_PER_DAY;
@@ -9951,8 +10032,8 @@ const NETWORKDAYS_INTL = {
9951
10032
  });
9952
10033
  }
9953
10034
  const invertDate = _startDate.getTime() > _endDate.getTime();
9954
- const stopDate = new Date((invertDate ? _startDate : _endDate).getTime());
9955
- let stepDate = new Date((invertDate ? _endDate : _startDate).getTime());
10035
+ const stopDate = DateTime.fromTimestamp((invertDate ? _startDate : _endDate).getTime());
10036
+ let stepDate = DateTime.fromTimestamp((invertDate ? _endDate : _startDate).getTime());
9956
10037
  const timeStopDate = stopDate.getTime();
9957
10038
  let timeStepDate = stepDate.getTime();
9958
10039
  let netWorkingDay = 0;
@@ -9978,8 +10059,7 @@ const NOW = {
9978
10059
  return this.locale.dateFormat + " " + this.locale.timeFormat;
9979
10060
  },
9980
10061
  compute: function () {
9981
- let today = new Date();
9982
- today.setMilliseconds(0);
10062
+ let today = DateTime.now();
9983
10063
  const delta = today.getTime() - INITIAL_1900_DAY.getTime();
9984
10064
  const time = today.getHours() / 24 + today.getMinutes() / 1440 + today.getSeconds() / 86400;
9985
10065
  return Math.floor(delta / MS_PER_DAY) + time;
@@ -10053,8 +10133,8 @@ const TODAY = {
10053
10133
  return this.locale.dateFormat;
10054
10134
  },
10055
10135
  compute: function () {
10056
- const today = new Date();
10057
- const jsDate = new Date(today.getFullYear(), today.getMonth(), today.getDate());
10136
+ const today = DateTime.now();
10137
+ const jsDate = new DateTime(today.getFullYear(), today.getMonth(), today.getDate());
10058
10138
  return jsDateToRoundNumber(jsDate);
10059
10139
  },
10060
10140
  isExported: true,
@@ -10109,10 +10189,10 @@ const WEEKNUM = {
10109
10189
  }
10110
10190
  const y = _date.getFullYear();
10111
10191
  let dayStart = 1;
10112
- let startDayOfFirstWeek = new Date(y, 0, dayStart);
10192
+ let startDayOfFirstWeek = new DateTime(y, 0, dayStart);
10113
10193
  while (startDayOfFirstWeek.getDay() !== startDayOfWeek) {
10114
10194
  dayStart += 1;
10115
- startDayOfFirstWeek = new Date(y, 0, dayStart);
10195
+ startDayOfFirstWeek = new DateTime(y, 0, dayStart);
10116
10196
  }
10117
10197
  const dif = (_date.getTime() - startDayOfFirstWeek.getTime()) / MS_PER_DAY;
10118
10198
  if (dif < 0) {
@@ -10170,7 +10250,7 @@ const WORKDAY_INTL = {
10170
10250
  timesHoliday.add(holiday.getTime());
10171
10251
  });
10172
10252
  }
10173
- let stepDate = new Date(_startDate.getTime());
10253
+ let stepDate = DateTime.fromTimestamp(_startDate.getTime());
10174
10254
  let timeStepDate = stepDate.getTime();
10175
10255
  const unitDay = Math.sign(_numDays);
10176
10256
  let stepDay = Math.abs(_numDays);
@@ -10234,7 +10314,7 @@ const MONTH_START = {
10234
10314
  const _startDate = toJsDate(date, this.locale);
10235
10315
  const yStart = _startDate.getFullYear();
10236
10316
  const mStart = _startDate.getMonth();
10237
- const jsDate = new Date(yStart, mStart, 1);
10317
+ const jsDate = new DateTime(yStart, mStart, 1);
10238
10318
  return jsDateToRoundNumber(jsDate);
10239
10319
  },
10240
10320
  };
@@ -10276,7 +10356,7 @@ const QUARTER_START = {
10276
10356
  compute: function (date) {
10277
10357
  const quarter = QUARTER.compute.bind(this)(date);
10278
10358
  const year = YEAR.compute.bind(this)(date);
10279
- const jsDate = new Date(year, (quarter - 1) * 3, 1);
10359
+ const jsDate = new DateTime(year, (quarter - 1) * 3, 1);
10280
10360
  return jsDateToRoundNumber(jsDate);
10281
10361
  },
10282
10362
  };
@@ -10293,7 +10373,7 @@ const QUARTER_END = {
10293
10373
  compute: function (date) {
10294
10374
  const quarter = QUARTER.compute.bind(this)(date);
10295
10375
  const year = YEAR.compute.bind(this)(date);
10296
- const jsDate = new Date(year, quarter * 3, 0);
10376
+ const jsDate = new DateTime(year, quarter * 3, 0);
10297
10377
  return jsDateToRoundNumber(jsDate);
10298
10378
  },
10299
10379
  };
@@ -10309,7 +10389,7 @@ const YEAR_START = {
10309
10389
  },
10310
10390
  compute: function (date) {
10311
10391
  const year = YEAR.compute.bind(this)(date);
10312
- const jsDate = new Date(year, 0, 1);
10392
+ const jsDate = new DateTime(year, 0, 1);
10313
10393
  return jsDateToRoundNumber(jsDate);
10314
10394
  },
10315
10395
  };
@@ -10325,7 +10405,7 @@ const YEAR_END = {
10325
10405
  },
10326
10406
  compute: function (date) {
10327
10407
  const year = YEAR.compute.bind(this)(date);
10328
- const jsDate = new Date(year + 1, 0, 0);
10408
+ const jsDate = new DateTime(year + 1, 0, 0);
10329
10409
  return jsDateToRoundNumber(jsDate);
10330
10410
  },
10331
10411
  };
@@ -10373,8 +10453,8 @@ const DEFAULT_DELTA_ARG = 0;
10373
10453
  const DELTA = {
10374
10454
  description: _lt("Compare two numeric values, returning 1 if they're equal."),
10375
10455
  args: [
10376
- arg(" (number)", _lt("The first number to compare.")),
10377
- arg(` (number, default=${DEFAULT_DELTA_ARG})`, _lt("The second number to compare.")),
10456
+ arg("number1 (number)", _lt("The first number to compare.")),
10457
+ arg(`number2 (number, default=${DEFAULT_DELTA_ARG})`, _lt("The second number to compare.")),
10378
10458
  ],
10379
10459
  returns: ["NUMBER"],
10380
10460
  compute: function (number1, number2 = DEFAULT_DELTA_ARG) {
@@ -10543,7 +10623,7 @@ function assertDeprecationFactorStrictlyPositive(factor) {
10543
10623
  function assertSettlementLessThanOneYearBeforeMaturity(settlement, maturity, locale) {
10544
10624
  const startDate = toJsDate(settlement, locale);
10545
10625
  const endDate = toJsDate(maturity, locale);
10546
- const startDatePlusOneYear = new Date(startDate);
10626
+ const startDatePlusOneYear = toJsDate(settlement, locale);
10547
10627
  startDatePlusOneYear.setFullYear(startDate.getFullYear() + 1);
10548
10628
  assert(() => endDate.getTime() <= startDatePlusOneYear.getTime(), _lt("The settlement date (%s) must at most one year after the maturity date (%s).", settlement.toString(), maturity.toString()));
10549
10629
  }
@@ -10678,7 +10758,7 @@ const AMORLINC = {
10678
10758
  arg("salvage (number)", _lt("The value of the asset at the end of depreciation.")),
10679
10759
  arg("period (number)", _lt("The single period within life for which to calculate depreciation.")),
10680
10760
  arg("rate (number)", _lt("The deprecation rate.")),
10681
- arg(" (number, optional)", _lt("An indicator of what day count method to use.")),
10761
+ arg("day_count_convention (number, optional)", _lt("An indicator of what day count method to use.")),
10682
10762
  ],
10683
10763
  returns: ["NUMBER"],
10684
10764
  compute: function (cost, purchaseDate, firstPeriodEnd, salvage, period, rate, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
@@ -13697,8 +13777,8 @@ const MID = {
13697
13777
  description: _lt("A segment of a string."),
13698
13778
  args: [
13699
13779
  arg("text (string)", _lt("The string to extract a segment from.")),
13700
- arg(" (number)", _lt("The index from the left of string from which to begin extracting. The first character in string has the index 1.")),
13701
- arg(" (number)", _lt("The length of the segment to extract.")),
13780
+ arg("starting_at (number)", _lt("The index from the left of string from which to begin extracting. The first character in string has the index 1.")),
13781
+ arg("extract_length (number)", _lt("The length of the segment to extract.")),
13702
13782
  ],
13703
13783
  returns: ["STRING"],
13704
13784
  compute: function (text, starting_at, extract_length) {
@@ -20494,10 +20574,10 @@ const arrowMap = {
20494
20574
  function updateSelectionWithArrowKeys(ev, selection) {
20495
20575
  const direction = arrowMap[ev.key];
20496
20576
  if (ev.shiftKey) {
20497
- selection.resizeAnchorZone(direction, ev.ctrlKey ? "end" : 1);
20577
+ selection.resizeAnchorZone(direction, isCtrlKey(ev) ? "end" : 1);
20498
20578
  }
20499
20579
  else {
20500
- selection.moveAnchorCell(direction, ev.ctrlKey ? "end" : 1);
20580
+ selection.moveAnchorCell(direction, isCtrlKey(ev) ? "end" : 1);
20501
20581
  }
20502
20582
  }
20503
20583
 
@@ -25542,7 +25622,10 @@ class GridOverlay extends owl.Component {
25542
25622
  return;
25543
25623
  }
25544
25624
  const [col, row] = this.getCartesianCoordinates(ev);
25545
- this.props.onCellClicked(col, row, { shiftKey: ev.shiftKey, ctrlKey: ev.ctrlKey });
25625
+ this.props.onCellClicked(col, row, {
25626
+ expandZone: ev.shiftKey,
25627
+ addZone: isCtrlKey(ev),
25628
+ });
25546
25629
  }
25547
25630
  onDoubleClick(ev) {
25548
25631
  const [col, row] = this.getCartesianCoordinates(ev);
@@ -25770,7 +25853,7 @@ class AbstractResizer extends owl.Component {
25770
25853
  this._increaseSelection(index);
25771
25854
  }
25772
25855
  else {
25773
- this._selectElement(index, ev.ctrlKey);
25856
+ this._selectElement(index, isCtrlKey(ev));
25774
25857
  }
25775
25858
  this.lastSelectedElementIndex = index;
25776
25859
  const mouseMoveSelect = (col, row) => {
@@ -25783,7 +25866,7 @@ class AbstractResizer extends owl.Component {
25783
25866
  const mouseUpSelect = () => {
25784
25867
  this.state.isSelecting = false;
25785
25868
  this.lastSelectedElementIndex = null;
25786
- this.env.model.dispatch(ev.ctrlKey ? "PREPARE_SELECTION_INPUT_EXPANSION" : "STOP_SELECTION_INPUT");
25869
+ this.env.model.dispatch(isCtrlKey(ev) ? "PREPARE_SELECTION_INPUT_EXPANSION" : "STOP_SELECTION_INPUT");
25787
25870
  this._computeGrabDisplay(ev);
25788
25871
  };
25789
25872
  dragAndDropBeyondTheViewport(this.env, mouseMoveSelect, mouseUpSelect);
@@ -25935,8 +26018,8 @@ class ColResizer extends AbstractResizer {
25935
26018
  this.env.raiseError(MergeErrorMessage);
25936
26019
  }
25937
26020
  }
25938
- _selectElement(index, ctrlKey) {
25939
- this.env.model.selection.selectColumn(index, ctrlKey ? "newAnchor" : "overrideSelection");
26021
+ _selectElement(index, addDistinctHeader) {
26022
+ this.env.model.selection.selectColumn(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
25940
26023
  }
25941
26024
  _increaseSelection(index) {
25942
26025
  this.env.model.selection.selectColumn(index, "updateAnchor");
@@ -26109,8 +26192,8 @@ class RowResizer extends AbstractResizer {
26109
26192
  this.env.raiseError(MergeErrorMessage);
26110
26193
  }
26111
26194
  }
26112
- _selectElement(index, ctrlKey) {
26113
- this.env.model.selection.selectRow(index, ctrlKey ? "newAnchor" : "overrideSelection");
26195
+ _selectElement(index, addDistinctHeader) {
26196
+ this.env.model.selection.selectRow(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
26114
26197
  }
26115
26198
  _increaseSelection(index) {
26116
26199
  this.env.model.selection.selectRow(index, "updateAnchor");
@@ -26916,8 +26999,8 @@ class Grid extends owl.Component {
26916
26999
  // ---------------------------------------------------------------------------
26917
27000
  // Zone selection with mouse
26918
27001
  // ---------------------------------------------------------------------------
26919
- onCellClicked(col, row, { ctrlKey, shiftKey }) {
26920
- if (ctrlKey) {
27002
+ onCellClicked(col, row, { addZone, expandZone }) {
27003
+ if (addZone) {
26921
27004
  this.env.model.dispatch("PREPARE_SELECTION_INPUT_EXPANSION");
26922
27005
  }
26923
27006
  if (this.env.model.getters.hasOpenedPopover()) {
@@ -26926,10 +27009,10 @@ class Grid extends owl.Component {
26926
27009
  if (this.env.model.getters.getEditionMode() === "editing") {
26927
27010
  this.env.model.dispatch("STOP_EDITION");
26928
27011
  }
26929
- if (shiftKey) {
27012
+ if (expandZone) {
26930
27013
  this.env.model.selection.setAnchorCorner(col, row);
26931
27014
  }
26932
- else if (ctrlKey) {
27015
+ else if (addZone) {
26933
27016
  this.env.model.selection.addCellToSelection(col, row);
26934
27017
  }
26935
27018
  else {
@@ -26991,9 +27074,7 @@ class Grid extends owl.Component {
26991
27074
  return;
26992
27075
  }
26993
27076
  let keyDownString = "";
26994
- if (ev.ctrlKey)
26995
- keyDownString += "CTRL+";
26996
- if (ev.metaKey)
27077
+ if (isCtrlKey(ev))
26997
27078
  keyDownString += "CTRL+";
26998
27079
  if (ev.altKey)
26999
27080
  keyDownString += "ALT+";
@@ -35562,56 +35643,758 @@ class CompilationParametersBuilder {
35562
35643
  }
35563
35644
  }
35564
35645
 
35646
+ function quickselect(arr, k, left, right, compare) {
35647
+ quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);
35648
+ }
35649
+
35650
+ function quickselectStep(arr, k, left, right, compare) {
35651
+
35652
+ while (right > left) {
35653
+ if (right - left > 600) {
35654
+ var n = right - left + 1;
35655
+ var m = k - left + 1;
35656
+ var z = Math.log(n);
35657
+ var s = 0.5 * Math.exp(2 * z / 3);
35658
+ var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
35659
+ var newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
35660
+ var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
35661
+ quickselectStep(arr, k, newLeft, newRight, compare);
35662
+ }
35663
+
35664
+ var t = arr[k];
35665
+ var i = left;
35666
+ var j = right;
35667
+
35668
+ swap(arr, left, k);
35669
+ if (compare(arr[right], t) > 0) swap(arr, left, right);
35670
+
35671
+ while (i < j) {
35672
+ swap(arr, i, j);
35673
+ i++;
35674
+ j--;
35675
+ while (compare(arr[i], t) < 0) i++;
35676
+ while (compare(arr[j], t) > 0) j--;
35677
+ }
35678
+
35679
+ if (compare(arr[left], t) === 0) swap(arr, left, j);
35680
+ else {
35681
+ j++;
35682
+ swap(arr, j, right);
35683
+ }
35684
+
35685
+ if (j <= k) left = j + 1;
35686
+ if (k <= j) right = j - 1;
35687
+ }
35688
+ }
35689
+
35690
+ function swap(arr, i, j) {
35691
+ var tmp = arr[i];
35692
+ arr[i] = arr[j];
35693
+ arr[j] = tmp;
35694
+ }
35695
+
35696
+ function defaultCompare(a, b) {
35697
+ return a < b ? -1 : a > b ? 1 : 0;
35698
+ }
35699
+
35700
+ class RBush {
35701
+ constructor(maxEntries = 9) {
35702
+ // max entries in a node is 9 by default; min node fill is 40% for best performance
35703
+ this._maxEntries = Math.max(4, maxEntries);
35704
+ this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
35705
+ this.clear();
35706
+ }
35707
+
35708
+ all() {
35709
+ return this._all(this.data, []);
35710
+ }
35711
+
35712
+ search(bbox) {
35713
+ let node = this.data;
35714
+ const result = [];
35715
+
35716
+ if (!intersects(bbox, node)) return result;
35717
+
35718
+ const toBBox = this.toBBox;
35719
+ const nodesToSearch = [];
35720
+
35721
+ while (node) {
35722
+ for (let i = 0; i < node.children.length; i++) {
35723
+ const child = node.children[i];
35724
+ const childBBox = node.leaf ? toBBox(child) : child;
35725
+
35726
+ if (intersects(bbox, childBBox)) {
35727
+ if (node.leaf) result.push(child);
35728
+ else if (contains(bbox, childBBox)) this._all(child, result);
35729
+ else nodesToSearch.push(child);
35730
+ }
35731
+ }
35732
+ node = nodesToSearch.pop();
35733
+ }
35734
+
35735
+ return result;
35736
+ }
35737
+
35738
+ collides(bbox) {
35739
+ let node = this.data;
35740
+
35741
+ if (!intersects(bbox, node)) return false;
35742
+
35743
+ const nodesToSearch = [];
35744
+ while (node) {
35745
+ for (let i = 0; i < node.children.length; i++) {
35746
+ const child = node.children[i];
35747
+ const childBBox = node.leaf ? this.toBBox(child) : child;
35748
+
35749
+ if (intersects(bbox, childBBox)) {
35750
+ if (node.leaf || contains(bbox, childBBox)) return true;
35751
+ nodesToSearch.push(child);
35752
+ }
35753
+ }
35754
+ node = nodesToSearch.pop();
35755
+ }
35756
+
35757
+ return false;
35758
+ }
35759
+
35760
+ load(data) {
35761
+ if (!(data && data.length)) return this;
35762
+
35763
+ if (data.length < this._minEntries) {
35764
+ for (let i = 0; i < data.length; i++) {
35765
+ this.insert(data[i]);
35766
+ }
35767
+ return this;
35768
+ }
35769
+
35770
+ // recursively build the tree with the given data from scratch using OMT algorithm
35771
+ let node = this._build(data.slice(), 0, data.length - 1, 0);
35772
+
35773
+ if (!this.data.children.length) {
35774
+ // save as is if tree is empty
35775
+ this.data = node;
35776
+
35777
+ } else if (this.data.height === node.height) {
35778
+ // split root if trees have the same height
35779
+ this._splitRoot(this.data, node);
35780
+
35781
+ } else {
35782
+ if (this.data.height < node.height) {
35783
+ // swap trees if inserted one is bigger
35784
+ const tmpNode = this.data;
35785
+ this.data = node;
35786
+ node = tmpNode;
35787
+ }
35788
+
35789
+ // insert the small tree into the large tree at appropriate level
35790
+ this._insert(node, this.data.height - node.height - 1, true);
35791
+ }
35792
+
35793
+ return this;
35794
+ }
35795
+
35796
+ insert(item) {
35797
+ if (item) this._insert(item, this.data.height - 1);
35798
+ return this;
35799
+ }
35800
+
35801
+ clear() {
35802
+ this.data = createNode([]);
35803
+ return this;
35804
+ }
35805
+
35806
+ remove(item, equalsFn) {
35807
+ if (!item) return this;
35808
+
35809
+ let node = this.data;
35810
+ const bbox = this.toBBox(item);
35811
+ const path = [];
35812
+ const indexes = [];
35813
+ let i, parent, goingUp;
35814
+
35815
+ // depth-first iterative tree traversal
35816
+ while (node || path.length) {
35817
+
35818
+ if (!node) { // go up
35819
+ node = path.pop();
35820
+ parent = path[path.length - 1];
35821
+ i = indexes.pop();
35822
+ goingUp = true;
35823
+ }
35824
+
35825
+ if (node.leaf) { // check current node
35826
+ const index = findItem(item, node.children, equalsFn);
35827
+
35828
+ if (index !== -1) {
35829
+ // item found, remove the item and condense tree upwards
35830
+ node.children.splice(index, 1);
35831
+ path.push(node);
35832
+ this._condense(path);
35833
+ return this;
35834
+ }
35835
+ }
35836
+
35837
+ if (!goingUp && !node.leaf && contains(node, bbox)) { // go down
35838
+ path.push(node);
35839
+ indexes.push(i);
35840
+ i = 0;
35841
+ parent = node;
35842
+ node = node.children[0];
35843
+
35844
+ } else if (parent) { // go right
35845
+ i++;
35846
+ node = parent.children[i];
35847
+ goingUp = false;
35848
+
35849
+ } else node = null; // nothing found
35850
+ }
35851
+
35852
+ return this;
35853
+ }
35854
+
35855
+ toBBox(item) { return item; }
35856
+
35857
+ compareMinX(a, b) { return a.minX - b.minX; }
35858
+ compareMinY(a, b) { return a.minY - b.minY; }
35859
+
35860
+ toJSON() { return this.data; }
35861
+
35862
+ fromJSON(data) {
35863
+ this.data = data;
35864
+ return this;
35865
+ }
35866
+
35867
+ _all(node, result) {
35868
+ const nodesToSearch = [];
35869
+ while (node) {
35870
+ if (node.leaf) result.push(...node.children);
35871
+ else nodesToSearch.push(...node.children);
35872
+
35873
+ node = nodesToSearch.pop();
35874
+ }
35875
+ return result;
35876
+ }
35877
+
35878
+ _build(items, left, right, height) {
35879
+
35880
+ const N = right - left + 1;
35881
+ let M = this._maxEntries;
35882
+ let node;
35883
+
35884
+ if (N <= M) {
35885
+ // reached leaf level; return leaf
35886
+ node = createNode(items.slice(left, right + 1));
35887
+ calcBBox(node, this.toBBox);
35888
+ return node;
35889
+ }
35890
+
35891
+ if (!height) {
35892
+ // target height of the bulk-loaded tree
35893
+ height = Math.ceil(Math.log(N) / Math.log(M));
35894
+
35895
+ // target number of root entries to maximize storage utilization
35896
+ M = Math.ceil(N / Math.pow(M, height - 1));
35897
+ }
35898
+
35899
+ node = createNode([]);
35900
+ node.leaf = false;
35901
+ node.height = height;
35902
+
35903
+ // split the items into M mostly square tiles
35904
+
35905
+ const N2 = Math.ceil(N / M);
35906
+ const N1 = N2 * Math.ceil(Math.sqrt(M));
35907
+
35908
+ multiSelect(items, left, right, N1, this.compareMinX);
35909
+
35910
+ for (let i = left; i <= right; i += N1) {
35911
+
35912
+ const right2 = Math.min(i + N1 - 1, right);
35913
+
35914
+ multiSelect(items, i, right2, N2, this.compareMinY);
35915
+
35916
+ for (let j = i; j <= right2; j += N2) {
35917
+
35918
+ const right3 = Math.min(j + N2 - 1, right2);
35919
+
35920
+ // pack each entry recursively
35921
+ node.children.push(this._build(items, j, right3, height - 1));
35922
+ }
35923
+ }
35924
+
35925
+ calcBBox(node, this.toBBox);
35926
+
35927
+ return node;
35928
+ }
35929
+
35930
+ _chooseSubtree(bbox, node, level, path) {
35931
+ while (true) {
35932
+ path.push(node);
35933
+
35934
+ if (node.leaf || path.length - 1 === level) break;
35935
+
35936
+ let minArea = Infinity;
35937
+ let minEnlargement = Infinity;
35938
+ let targetNode;
35939
+
35940
+ for (let i = 0; i < node.children.length; i++) {
35941
+ const child = node.children[i];
35942
+ const area = bboxArea(child);
35943
+ const enlargement = enlargedArea(bbox, child) - area;
35944
+
35945
+ // choose entry with the least area enlargement
35946
+ if (enlargement < minEnlargement) {
35947
+ minEnlargement = enlargement;
35948
+ minArea = area < minArea ? area : minArea;
35949
+ targetNode = child;
35950
+
35951
+ } else if (enlargement === minEnlargement) {
35952
+ // otherwise choose one with the smallest area
35953
+ if (area < minArea) {
35954
+ minArea = area;
35955
+ targetNode = child;
35956
+ }
35957
+ }
35958
+ }
35959
+
35960
+ node = targetNode || node.children[0];
35961
+ }
35962
+
35963
+ return node;
35964
+ }
35965
+
35966
+ _insert(item, level, isNode) {
35967
+ const bbox = isNode ? item : this.toBBox(item);
35968
+ const insertPath = [];
35969
+
35970
+ // find the best node for accommodating the item, saving all nodes along the path too
35971
+ const node = this._chooseSubtree(bbox, this.data, level, insertPath);
35972
+
35973
+ // put the item into the node
35974
+ node.children.push(item);
35975
+ extend(node, bbox);
35976
+
35977
+ // split on node overflow; propagate upwards if necessary
35978
+ while (level >= 0) {
35979
+ if (insertPath[level].children.length > this._maxEntries) {
35980
+ this._split(insertPath, level);
35981
+ level--;
35982
+ } else break;
35983
+ }
35984
+
35985
+ // adjust bboxes along the insertion path
35986
+ this._adjustParentBBoxes(bbox, insertPath, level);
35987
+ }
35988
+
35989
+ // split overflowed node into two
35990
+ _split(insertPath, level) {
35991
+ const node = insertPath[level];
35992
+ const M = node.children.length;
35993
+ const m = this._minEntries;
35994
+
35995
+ this._chooseSplitAxis(node, m, M);
35996
+
35997
+ const splitIndex = this._chooseSplitIndex(node, m, M);
35998
+
35999
+ const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
36000
+ newNode.height = node.height;
36001
+ newNode.leaf = node.leaf;
36002
+
36003
+ calcBBox(node, this.toBBox);
36004
+ calcBBox(newNode, this.toBBox);
36005
+
36006
+ if (level) insertPath[level - 1].children.push(newNode);
36007
+ else this._splitRoot(node, newNode);
36008
+ }
36009
+
36010
+ _splitRoot(node, newNode) {
36011
+ // split root node
36012
+ this.data = createNode([node, newNode]);
36013
+ this.data.height = node.height + 1;
36014
+ this.data.leaf = false;
36015
+ calcBBox(this.data, this.toBBox);
36016
+ }
36017
+
36018
+ _chooseSplitIndex(node, m, M) {
36019
+ let index;
36020
+ let minOverlap = Infinity;
36021
+ let minArea = Infinity;
36022
+
36023
+ for (let i = m; i <= M - m; i++) {
36024
+ const bbox1 = distBBox(node, 0, i, this.toBBox);
36025
+ const bbox2 = distBBox(node, i, M, this.toBBox);
36026
+
36027
+ const overlap = intersectionArea(bbox1, bbox2);
36028
+ const area = bboxArea(bbox1) + bboxArea(bbox2);
36029
+
36030
+ // choose distribution with minimum overlap
36031
+ if (overlap < minOverlap) {
36032
+ minOverlap = overlap;
36033
+ index = i;
36034
+
36035
+ minArea = area < minArea ? area : minArea;
36036
+
36037
+ } else if (overlap === minOverlap) {
36038
+ // otherwise choose distribution with minimum area
36039
+ if (area < minArea) {
36040
+ minArea = area;
36041
+ index = i;
36042
+ }
36043
+ }
36044
+ }
36045
+
36046
+ return index || M - m;
36047
+ }
36048
+
36049
+ // sorts node children by the best axis for split
36050
+ _chooseSplitAxis(node, m, M) {
36051
+ const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;
36052
+ const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;
36053
+ const xMargin = this._allDistMargin(node, m, M, compareMinX);
36054
+ const yMargin = this._allDistMargin(node, m, M, compareMinY);
36055
+
36056
+ // if total distributions margin value is minimal for x, sort by minX,
36057
+ // otherwise it's already sorted by minY
36058
+ if (xMargin < yMargin) node.children.sort(compareMinX);
36059
+ }
36060
+
36061
+ // total margin of all possible split distributions where each node is at least m full
36062
+ _allDistMargin(node, m, M, compare) {
36063
+ node.children.sort(compare);
36064
+
36065
+ const toBBox = this.toBBox;
36066
+ const leftBBox = distBBox(node, 0, m, toBBox);
36067
+ const rightBBox = distBBox(node, M - m, M, toBBox);
36068
+ let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);
36069
+
36070
+ for (let i = m; i < M - m; i++) {
36071
+ const child = node.children[i];
36072
+ extend(leftBBox, node.leaf ? toBBox(child) : child);
36073
+ margin += bboxMargin(leftBBox);
36074
+ }
36075
+
36076
+ for (let i = M - m - 1; i >= m; i--) {
36077
+ const child = node.children[i];
36078
+ extend(rightBBox, node.leaf ? toBBox(child) : child);
36079
+ margin += bboxMargin(rightBBox);
36080
+ }
36081
+
36082
+ return margin;
36083
+ }
36084
+
36085
+ _adjustParentBBoxes(bbox, path, level) {
36086
+ // adjust bboxes along the given tree path
36087
+ for (let i = level; i >= 0; i--) {
36088
+ extend(path[i], bbox);
36089
+ }
36090
+ }
36091
+
36092
+ _condense(path) {
36093
+ // go through the path, removing empty nodes and updating bboxes
36094
+ for (let i = path.length - 1, siblings; i >= 0; i--) {
36095
+ if (path[i].children.length === 0) {
36096
+ if (i > 0) {
36097
+ siblings = path[i - 1].children;
36098
+ siblings.splice(siblings.indexOf(path[i]), 1);
36099
+
36100
+ } else this.clear();
36101
+
36102
+ } else calcBBox(path[i], this.toBBox);
36103
+ }
36104
+ }
36105
+ }
36106
+
36107
+ function findItem(item, items, equalsFn) {
36108
+ if (!equalsFn) return items.indexOf(item);
36109
+
36110
+ for (let i = 0; i < items.length; i++) {
36111
+ if (equalsFn(item, items[i])) return i;
36112
+ }
36113
+ return -1;
36114
+ }
36115
+
36116
+ // calculate node's bbox from bboxes of its children
36117
+ function calcBBox(node, toBBox) {
36118
+ distBBox(node, 0, node.children.length, toBBox, node);
36119
+ }
36120
+
36121
+ // min bounding rectangle of node children from k to p-1
36122
+ function distBBox(node, k, p, toBBox, destNode) {
36123
+ if (!destNode) destNode = createNode(null);
36124
+ destNode.minX = Infinity;
36125
+ destNode.minY = Infinity;
36126
+ destNode.maxX = -Infinity;
36127
+ destNode.maxY = -Infinity;
36128
+
36129
+ for (let i = k; i < p; i++) {
36130
+ const child = node.children[i];
36131
+ extend(destNode, node.leaf ? toBBox(child) : child);
36132
+ }
36133
+
36134
+ return destNode;
36135
+ }
36136
+
36137
+ function extend(a, b) {
36138
+ a.minX = Math.min(a.minX, b.minX);
36139
+ a.minY = Math.min(a.minY, b.minY);
36140
+ a.maxX = Math.max(a.maxX, b.maxX);
36141
+ a.maxY = Math.max(a.maxY, b.maxY);
36142
+ return a;
36143
+ }
36144
+
36145
+ function compareNodeMinX(a, b) { return a.minX - b.minX; }
36146
+ function compareNodeMinY(a, b) { return a.minY - b.minY; }
36147
+
36148
+ function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); }
36149
+ function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }
36150
+
36151
+ function enlargedArea(a, b) {
36152
+ return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *
36153
+ (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
36154
+ }
36155
+
36156
+ function intersectionArea(a, b) {
36157
+ const minX = Math.max(a.minX, b.minX);
36158
+ const minY = Math.max(a.minY, b.minY);
36159
+ const maxX = Math.min(a.maxX, b.maxX);
36160
+ const maxY = Math.min(a.maxY, b.maxY);
36161
+
36162
+ return Math.max(0, maxX - minX) *
36163
+ Math.max(0, maxY - minY);
36164
+ }
36165
+
36166
+ function contains(a, b) {
36167
+ return a.minX <= b.minX &&
36168
+ a.minY <= b.minY &&
36169
+ b.maxX <= a.maxX &&
36170
+ b.maxY <= a.maxY;
36171
+ }
36172
+
36173
+ function intersects(a, b) {
36174
+ return b.minX <= a.maxX &&
36175
+ b.minY <= a.maxY &&
36176
+ b.maxX >= a.minX &&
36177
+ b.maxY >= a.minY;
36178
+ }
36179
+
36180
+ function createNode(children) {
36181
+ return {
36182
+ children,
36183
+ height: 1,
36184
+ leaf: true,
36185
+ minX: Infinity,
36186
+ minY: Infinity,
36187
+ maxX: -Infinity,
36188
+ maxY: -Infinity
36189
+ };
36190
+ }
36191
+
36192
+ // sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
36193
+ // combines selection algorithm with binary divide & conquer approach
36194
+
36195
+ function multiSelect(arr, left, right, n, compare) {
36196
+ const stack = [left, right];
36197
+
36198
+ while (stack.length) {
36199
+ right = stack.pop();
36200
+ left = stack.pop();
36201
+
36202
+ if (right - left <= n) continue;
36203
+
36204
+ const mid = left + Math.ceil((right - left) / n / 2) * n;
36205
+ quickselect(arr, mid, left, right, compare);
36206
+
36207
+ stack.push(left, mid, mid, right);
36208
+ }
36209
+ }
36210
+
36211
+ /**
36212
+ * R-Tree Data Structure
36213
+ *
36214
+ * R-Tree is a spatial data structure used for efficient indexing and querying
36215
+ * of multi-dimensional objects, particularly in geometric and spatial applications.
36216
+ *
36217
+ * It organizes objects into a tree hierarchy, grouping nearby objects together
36218
+ * in bounding boxes. Each node in the tree represents a bounding box that
36219
+ * contains its child nodes or leaf objects. This hierarchical structure allows
36220
+ * for faster spatial queries.
36221
+ *
36222
+ * @see https://en.wikipedia.org/wiki/R-tree
36223
+ *
36224
+ * Consider a 2D Space with four zones: A, B, C, D
36225
+ * +--------------------------+
36226
+ * | |
36227
+ * | +---+ +-------+ |
36228
+ * | | A | | B | |
36229
+ * | +---+ +-------+ |
36230
+ * | |
36231
+ * | |
36232
+ * | +---+ |
36233
+ * | | C | |
36234
+ * | +---+ |
36235
+ * | +-----------+ |
36236
+ * | | D | |
36237
+ * | +-----------+ |
36238
+ * | |
36239
+ * +--------------------------+
36240
+ *
36241
+ * It groups together zones that are spatially close into a minimum bounding box.
36242
+ * For example, A and B are grouped together in rectangle R1, and C and D are grouped
36243
+ * in R2.
36244
+ *
36245
+ * R0
36246
+ * +--------------------------+
36247
+ * | R1 |
36248
+ * | +-----------------+ |
36249
+ * | | A | | B | |
36250
+ * | +-----------------+ |
36251
+ * | |
36252
+ * | R2 |
36253
+ * | +---+---+---+ |
36254
+ * | | | C | | |
36255
+ * | | +---+ | |
36256
+ * | +-----------+ |
36257
+ * | | D | |
36258
+ * | +-----------+ |
36259
+ * | |
36260
+ * +--------------------------+
36261
+ *
36262
+ * The tree would look like this:
36263
+ * R0
36264
+ * / \
36265
+ * / \
36266
+ * R1 R2
36267
+ * | |
36268
+ * A,B C,D
36269
+
36270
+ * Choosing how to group the zones is crucial for the performance of the tree.
36271
+ * Key considerations include avoiding excessive empty space coverage and minimizing overlap
36272
+ * to reduce the number of subtrees processed during searches.
36273
+ *
36274
+ * Various heuristics exist for determining the optimal grouping strategy, such as "least enlargement"
36275
+ * which prioritizes grouping nodes resulting in the smallest increase in bounding box size. In cases where
36276
+ * the choice cannot be made based on this criterion due to the same enlargement for different groupings,
36277
+ * we then evaluate "least area," aiming to minimize the overall area of bounding boxes.
36278
+ *
36279
+ * This implementation is tailored for spreadsheet use, indexing objects associated
36280
+ * with a zone and a sheet.
36281
+ *
36282
+ * It uses the RBush library under the hood. One 2D RBush R-tree per sheet.
36283
+ * @see https://github.com/mourner/rbush
36284
+ */
36285
+ class SpreadsheetRTree {
36286
+ /**
36287
+ * One 2D R-tree per sheet
36288
+ */
36289
+ rTrees = {};
36290
+ /**
36291
+ * Bulk-inserts the given items into the tree. Bulk insertion is usually ~2-3 times
36292
+ * faster than inserting items one by one. After bulk loading (bulk insertion into
36293
+ * an empty tree), subsequent query performance is also ~20-30% better.
36294
+ */
36295
+ constructor(items = []) {
36296
+ const rangesPerSheet = {};
36297
+ for (const item of items) {
36298
+ const sheetId = item.boundingBox.sheetId;
36299
+ if (!rangesPerSheet[sheetId]) {
36300
+ rangesPerSheet[sheetId] = [];
36301
+ }
36302
+ rangesPerSheet[sheetId].push(item);
36303
+ }
36304
+ for (const sheetId in rangesPerSheet) {
36305
+ this.rTrees[sheetId] = new ZoneRBush();
36306
+ this.rTrees[sheetId].load(rangesPerSheet[sheetId]); // bulk-insert
36307
+ }
36308
+ }
36309
+ insert(item) {
36310
+ const sheetId = item.boundingBox.sheetId;
36311
+ if (!this.rTrees[sheetId]) {
36312
+ this.rTrees[sheetId] = new ZoneRBush();
36313
+ }
36314
+ this.rTrees[sheetId].insert(item);
36315
+ }
36316
+ search({ zone, sheetId }) {
36317
+ if (!this.rTrees[sheetId]) {
36318
+ return [];
36319
+ }
36320
+ return this.rTrees[sheetId].search({
36321
+ minX: zone.left,
36322
+ minY: zone.top,
36323
+ maxX: zone.right,
36324
+ maxY: zone.bottom,
36325
+ });
36326
+ }
36327
+ remove(item) {
36328
+ const sheetId = item.boundingBox.sheetId;
36329
+ if (!this.rTrees[sheetId]) {
36330
+ return;
36331
+ }
36332
+ this.rTrees[sheetId].remove(item, deepEquals);
36333
+ }
36334
+ }
36335
+ /**
36336
+ * RBush extension to use zones as bounding boxes
36337
+ */
36338
+ class ZoneRBush extends RBush {
36339
+ toBBox({ boundingBox }) {
36340
+ const zone = boundingBox.zone;
36341
+ return {
36342
+ minX: zone.left,
36343
+ minY: zone.top,
36344
+ maxX: zone.right,
36345
+ maxY: zone.bottom,
36346
+ };
36347
+ }
36348
+ compareMinX(a, b) {
36349
+ return a.boundingBox.zone.left - b.boundingBox.zone.left;
36350
+ }
36351
+ compareMinY(a, b) {
36352
+ return a.boundingBox.zone.top - b.boundingBox.zone.top;
36353
+ }
36354
+ }
36355
+
35565
36356
  /**
35566
- * This class is an implementation of a dependency Graph.
36357
+ * Implementation of a dependency Graph.
35567
36358
  * The graph is used to evaluate the cells in the correct
35568
36359
  * order, and should be updated each time a cell's content is modified
35569
36360
  *
36361
+ * It uses an R-Tree data structure to efficiently find dependent cells.
35570
36362
  */
35571
36363
  class FormulaDependencyGraph {
35572
- /**
35573
- * Internal structure:
35574
- * - key: a cell position (encoded as an integer)
35575
- * - value: a set of cell positions that depends on the key
35576
- *
35577
- * Given
35578
- * - A1:"= B1 + SQRT(B2)"
35579
- * - C1:"= B1";
35580
- * - C2:"= C1"
35581
- *
35582
- * we will have something like:
35583
- * - B1 ---> (A1, C1) meaning A1 and C1 depends on B1
35584
- * - B2 ---> (A1) meaning A1 depends on B2
35585
- * - C1 ---> (C2) meaning C2 depends on C1
35586
- */
35587
- inverseDependencies = new Map();
36364
+ encoder;
35588
36365
  dependencies = new Map();
36366
+ rTree;
36367
+ constructor(encoder, data = []) {
36368
+ this.encoder = encoder;
36369
+ this.rTree = new SpreadsheetRTree(data);
36370
+ }
35589
36371
  removeAllDependencies(formulaPositionId) {
35590
- const dependencies = this.dependencies.get(formulaPositionId);
35591
- if (!dependencies) {
36372
+ const ranges = this.dependencies.get(formulaPositionId);
36373
+ if (!ranges) {
35592
36374
  return;
35593
36375
  }
35594
- for (const dependency of dependencies) {
35595
- this.inverseDependencies.get(dependency)?.delete(formulaPositionId);
36376
+ for (const range of ranges) {
36377
+ this.rTree.remove(range);
35596
36378
  }
35597
36379
  this.dependencies.delete(formulaPositionId);
35598
36380
  }
35599
36381
  addDependencies(formulaPositionId, dependencies) {
35600
- for (const dependency of dependencies) {
35601
- const inverseDependencies = this.inverseDependencies.get(dependency);
35602
- if (inverseDependencies) {
35603
- inverseDependencies.add(formulaPositionId);
35604
- }
35605
- else {
35606
- this.inverseDependencies.set(dependency, new Set([formulaPositionId]));
35607
- }
36382
+ const rTreeItems = dependencies.map(({ sheetId, zone }) => ({
36383
+ data: formulaPositionId,
36384
+ boundingBox: {
36385
+ zone,
36386
+ sheetId,
36387
+ },
36388
+ }));
36389
+ for (const item of rTreeItems) {
36390
+ this.rTree.insert(item);
35608
36391
  }
35609
36392
  const existingDependencies = this.dependencies.get(formulaPositionId);
35610
36393
  if (existingDependencies) {
35611
- existingDependencies.push(...dependencies);
36394
+ existingDependencies.push(...rTreeItems);
35612
36395
  }
35613
36396
  else {
35614
- this.dependencies.set(formulaPositionId, dependencies);
36397
+ this.dependencies.set(formulaPositionId, rTreeItems);
35615
36398
  }
35616
36399
  }
35617
36400
  /**
@@ -35619,20 +36402,20 @@ class FormulaDependencyGraph {
35619
36402
  * in the correct order they should be evaluated.
35620
36403
  * This is called a topological ordering (excluding cycles)
35621
36404
  */
35622
- getCellsDependingOn(positionIds) {
36405
+ getCellsDependingOn(ranges) {
35623
36406
  const visited = new JetSet();
35624
- const queue = Array.from(positionIds).reverse();
36407
+ const queue = Array.from(ranges).reverse();
35625
36408
  while (queue.length > 0) {
35626
- const node = queue.pop();
35627
- visited.add(node);
35628
- const adjacentNodes = this.inverseDependencies.get(node) || new Set();
35629
- for (const adjacentNode of adjacentNodes) {
35630
- if (!visited.has(adjacentNode)) {
35631
- queue.push(adjacentNode);
36409
+ const range = queue.pop();
36410
+ visited.add(...this.encoder.encodeBoundingBox(range));
36411
+ const impactedPositionIds = this.rTree.search(range).map((dep) => dep.data);
36412
+ for (const positionId of impactedPositionIds) {
36413
+ if (!visited.has(positionId)) {
36414
+ queue.push(this.encoder.decodeToBoundingBox(positionId));
35632
36415
  }
35633
36416
  }
35634
36417
  }
35635
- visited.delete(...positionIds);
36418
+ visited.delete(...ranges.flatMap((r) => this.encoder.encodeBoundingBox(r)));
35636
36419
  return visited;
35637
36420
  }
35638
36421
  }
@@ -35720,9 +36503,9 @@ class Evaluator {
35720
36503
  context;
35721
36504
  getters;
35722
36505
  compilationParams;
35723
- positionEncoder = new PositionBitsEncoder();
36506
+ encoder = new PositionBitsEncoder();
35724
36507
  evaluatedCells = new Map();
35725
- formulaDependencies = lazy(new FormulaDependencyGraph());
36508
+ formulaDependencies = lazy(new FormulaDependencyGraph(this.encoder));
35726
36509
  blockedArrayFormulas = new Set();
35727
36510
  spreadingRelations = new SpreadingRelation();
35728
36511
  constructor(context, getters) {
@@ -35731,16 +36514,16 @@ class Evaluator {
35731
36514
  this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
35732
36515
  }
35733
36516
  getEvaluatedCell(position) {
35734
- return (this.evaluatedCells.get(this.encodePosition(position)) ||
36517
+ return (this.evaluatedCells.get(this.encoder.encode(position)) ||
35735
36518
  createEvaluatedCell("", { locale: this.getters.getLocale() }));
35736
36519
  }
35737
36520
  getArrayFormulaSpreadingOn(position) {
35738
- const positionId = this.encodePosition(position);
36521
+ const positionId = this.encoder.encode(position);
35739
36522
  const formulaPosition = this.getArrayFormulaSpreadingOnId(positionId);
35740
- return formulaPosition !== undefined ? this.decodePosition(formulaPosition) : undefined;
36523
+ return formulaPosition !== undefined ? this.encoder.decode(formulaPosition) : undefined;
35741
36524
  }
35742
36525
  getEvaluatedPositions() {
35743
- return [...this.evaluatedCells.keys()].map(this.decodePosition.bind(this));
36526
+ return [...this.evaluatedCells.keys()].map((p) => this.encoder.decode(p));
35744
36527
  }
35745
36528
  getArrayFormulaSpreadingOnId(positionId) {
35746
36529
  if (!this.spreadingRelations.hasArrayFormulaResult(positionId)) {
@@ -35750,7 +36533,7 @@ class Evaluator {
35750
36533
  return Array.from(arrayFormulas).find((positionId) => !this.blockedArrayFormulas.has(positionId));
35751
36534
  }
35752
36535
  updateDependencies(position) {
35753
- const positionId = this.encodePosition(position);
36536
+ const positionId = this.encoder.encode(position);
35754
36537
  this.formulaDependencies().removeAllDependencies(positionId);
35755
36538
  const dependencies = this.getDirectDependencies(positionId);
35756
36539
  this.formulaDependencies().addDependencies(positionId, dependencies);
@@ -35760,12 +36543,12 @@ class Evaluator {
35760
36543
  this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
35761
36544
  }
35762
36545
  evaluateCells(positions) {
35763
- const cells = positions.map(this.encodePosition.bind(this));
36546
+ const cells = positions.map((p) => this.encoder.encode(p));
35764
36547
  const cellsToCompute = new JetSet(cells);
35765
- const arrayFormulas = this.getArrayFormulasImpactedByChangesOf(cells);
36548
+ const arrayFormulasPositionIds = this.getArrayFormulasImpactedByChangesOf(cells);
35766
36549
  cellsToCompute.add(...this.getCellsDependingOn(cells));
35767
- cellsToCompute.add(...arrayFormulas);
35768
- cellsToCompute.add(...this.getCellsDependingOn(arrayFormulas));
36550
+ cellsToCompute.add(...arrayFormulasPositionIds);
36551
+ cellsToCompute.add(...this.getCellsDependingOn(arrayFormulasPositionIds));
35769
36552
  this.evaluate(cellsToCompute);
35770
36553
  }
35771
36554
  getArrayFormulasImpactedByChangesOf(positionIds) {
@@ -35788,12 +36571,14 @@ class Evaluator {
35788
36571
  this.blockedArrayFormulas = new Set();
35789
36572
  this.spreadingRelations = new SpreadingRelation();
35790
36573
  this.formulaDependencies = lazy(() => {
35791
- const dependencyGraph = new FormulaDependencyGraph();
35792
- for (const positionId of this.getAllCells()) {
35793
- const dependencies = this.getDirectDependencies(positionId);
35794
- dependencyGraph.addDependencies(positionId, dependencies);
35795
- }
35796
- return dependencyGraph;
36574
+ const dependencies = [...this.getAllCells()].flatMap((positionId) => this.getDirectDependencies(positionId).map((range) => ({
36575
+ data: positionId,
36576
+ boundingBox: {
36577
+ zone: range.zone,
36578
+ sheetId: range.sheetId,
36579
+ },
36580
+ })));
36581
+ return new FormulaDependencyGraph(this.encoder, dependencies);
35797
36582
  });
35798
36583
  }
35799
36584
  evaluateAllCells() {
@@ -35814,7 +36599,7 @@ class Evaluator {
35814
36599
  for (const sheetId of this.getters.getSheetIds()) {
35815
36600
  const cellIds = this.getters.getCells(sheetId);
35816
36601
  for (const cellId in cellIds) {
35817
- positionIds.add(this.encodePosition(this.getters.getCellPosition(cellId)));
36602
+ positionIds.add(this.encoder.encode(this.getters.getCellPosition(cellId)));
35818
36603
  }
35819
36604
  }
35820
36605
  return positionIds;
@@ -35881,7 +36666,7 @@ class Evaluator {
35881
36666
  }
35882
36667
  }
35883
36668
  computeAndSave(position) {
35884
- const positionId = this.encodePosition(position);
36669
+ const positionId = this.encoder.encode(position);
35885
36670
  const evaluatedCell = this.computeCell(positionId);
35886
36671
  if (!this.evaluatedCells.has(positionId)) {
35887
36672
  this.setEvaluatedCell(positionId, evaluatedCell);
@@ -35942,15 +36727,15 @@ class Evaluator {
35942
36727
  throw new Error(_lt("Result couldn't be automatically expanded. Please insert more columns and rows."));
35943
36728
  }
35944
36729
  updateSpreadRelation({ sheetId, col, row, }) {
35945
- const arrayFormulaPositionId = this.encodePosition({ sheetId, col, row });
36730
+ const arrayFormulaPositionId = this.encoder.encode({ sheetId, col, row });
35946
36731
  return (i, j) => {
35947
36732
  const position = { sheetId, col: i + col, row: j + row };
35948
- const resultPositionId = this.encodePosition(position);
36733
+ const resultPositionId = this.encoder.encode(position);
35949
36734
  this.spreadingRelations.addRelation({ resultPositionId, arrayFormulaPositionId });
35950
36735
  };
35951
36736
  }
35952
36737
  checkCollision({ sheetId, col, row }) {
35953
- const formulaPositionId = this.encodePosition({ sheetId, col, row });
36738
+ const formulaPositionId = this.encoder.encode({ sheetId, col, row });
35954
36739
  return (i, j) => {
35955
36740
  const position = { sheetId: sheetId, col: i + col, row: j + row };
35956
36741
  const rawCell = this.getters.getCell(position);
@@ -35972,7 +36757,7 @@ class Evaluator {
35972
36757
  format: format || formatFromPosition(i, j),
35973
36758
  locale: this.getters.getLocale(),
35974
36759
  });
35975
- const positionId = this.encodePosition(position);
36760
+ const positionId = this.encoder.encode(position);
35976
36761
  this.setEvaluatedCell(positionId, evaluatedCell);
35977
36762
  // check if formula dependencies present in the spread zone
35978
36763
  // if so, they need to be recomputed
@@ -36004,29 +36789,17 @@ class Evaluator {
36004
36789
  if (!cell?.isFormula) {
36005
36790
  return [];
36006
36791
  }
36007
- const dependencies = [];
36008
- for (const range of cell.dependencies) {
36009
- if (range.invalidSheetName || range.invalidXc) {
36010
- continue;
36011
- }
36012
- const sheetId = range.sheetId;
36013
- forEachPositionsInZone(range.zone, (col, row) => {
36014
- dependencies.push(this.encodePosition({ sheetId, col, row }));
36015
- });
36016
- }
36017
- return dependencies;
36792
+ return cell.dependencies;
36018
36793
  }
36019
36794
  getCellsDependingOn(positionIds) {
36020
- return this.formulaDependencies().getCellsDependingOn(positionIds);
36795
+ const ranges = [];
36796
+ for (const positionId of positionIds) {
36797
+ ranges.push(this.encoder.decodeToBoundingBox(positionId));
36798
+ }
36799
+ return this.formulaDependencies().getCellsDependingOn(ranges);
36021
36800
  }
36022
36801
  getCell(positionId) {
36023
- return this.getters.getCell(this.decodePosition(positionId));
36024
- }
36025
- encodePosition(position) {
36026
- return this.positionEncoder.encode(position);
36027
- }
36028
- decodePosition(positionId) {
36029
- return this.positionEncoder.decode(positionId);
36802
+ return this.getters.getCell(this.encoder.decode(positionId));
36030
36803
  }
36031
36804
  }
36032
36805
  function forEachSpreadPositionInMatrix(matrix, callback) {
@@ -36101,6 +36874,13 @@ class PositionBitsEncoder {
36101
36874
  encode({ sheetId, col, row }) {
36102
36875
  return (this.encodeSheet(sheetId) << 42n) | (BigInt(col) << 21n) | BigInt(row);
36103
36876
  }
36877
+ encodeBoundingBox({ sheetId, zone }) {
36878
+ const positions = [];
36879
+ forEachPositionsInZone(zone, (col, row) => {
36880
+ positions.push(this.encode({ sheetId, col, row }));
36881
+ });
36882
+ return positions;
36883
+ }
36104
36884
  decode(id) {
36105
36885
  // keep only the last 21 bits by AND-ing the bit sequence with 21 ones
36106
36886
  const row = Number(id & 2097151n);
@@ -36108,6 +36888,10 @@ class PositionBitsEncoder {
36108
36888
  const sheetId = this.decodeSheet(id >> 42n);
36109
36889
  return { sheetId, col, row };
36110
36890
  }
36891
+ decodeToBoundingBox(id) {
36892
+ const { sheetId, col, row } = this.decode(id);
36893
+ return { sheetId, zone: { left: col, top: row, right: col, bottom: row } };
36894
+ }
36111
36895
  encodeSheet(sheetId) {
36112
36896
  const sheetKey = this.sheetMapping[sheetId];
36113
36897
  if (sheetKey === undefined) {
@@ -36222,6 +37006,7 @@ class PositionBitsEncoder {
36222
37006
  class EvaluationPlugin extends UIPlugin {
36223
37007
  static getters = [
36224
37008
  "evaluateFormula",
37009
+ "getCorrespondingFormulaCell",
36225
37010
  "getRangeFormattedValues",
36226
37011
  "getRangeValues",
36227
37012
  "getRangeFormats",
@@ -36275,7 +37060,12 @@ class EvaluationPlugin extends UIPlugin {
36275
37060
  // Getters
36276
37061
  // ---------------------------------------------------------------------------
36277
37062
  evaluateFormula(sheetId, formulaString) {
36278
- return this.evaluator.evaluateFormula(sheetId, formulaString);
37063
+ try {
37064
+ return this.evaluator.evaluateFormula(sheetId, formulaString);
37065
+ }
37066
+ catch (error) {
37067
+ return error instanceof EvaluationError ? error.errorType : CellErrorType.GenericError;
37068
+ }
36279
37069
  }
36280
37070
  /**
36281
37071
  * Return the value of each cell in the range as they are displayed in the grid.
@@ -36665,34 +37455,29 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
36665
37455
  getComputedStyles(sheetId) {
36666
37456
  const computedStyle = {};
36667
37457
  for (let cf of this.getters.getConditionalFormats(sheetId).reverse()) {
36668
- try {
36669
- switch (cf.rule.type) {
36670
- case "ColorScaleRule":
36671
- for (let range of cf.ranges) {
36672
- this.applyColorScale(sheetId, range, cf.rule, computedStyle);
36673
- }
36674
- break;
36675
- case "CellIsRule":
36676
- for (let ref of cf.ranges) {
36677
- const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
36678
- for (let row = zone.top; row <= zone.bottom; row++) {
36679
- for (let col = zone.left; col <= zone.right; col++) {
36680
- const pr = this.rulePredicate[cf.rule.type];
36681
- let cell = this.getters.getEvaluatedCell({ sheetId, col, row });
36682
- if (pr && pr(cell, cf.rule)) {
36683
- if (!computedStyle[col])
36684
- computedStyle[col] = [];
36685
- // we must combine all the properties of all the CF rules applied to the given cell
36686
- computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
36687
- }
37458
+ switch (cf.rule.type) {
37459
+ case "ColorScaleRule":
37460
+ for (let range of cf.ranges) {
37461
+ this.applyColorScale(sheetId, range, cf.rule, computedStyle);
37462
+ }
37463
+ break;
37464
+ case "CellIsRule":
37465
+ for (let ref of cf.ranges) {
37466
+ const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
37467
+ for (let row = zone.top; row <= zone.bottom; row++) {
37468
+ for (let col = zone.left; col <= zone.right; col++) {
37469
+ const pr = this.rulePredicate[cf.rule.type];
37470
+ let cell = this.getters.getEvaluatedCell({ sheetId, col, row });
37471
+ if (pr && pr(cell, cf.rule)) {
37472
+ if (!computedStyle[col])
37473
+ computedStyle[col] = [];
37474
+ // we must combine all the properties of all the CF rules applied to the given cell
37475
+ computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
36688
37476
  }
36689
37477
  }
36690
37478
  }
36691
- break;
36692
- }
36693
- }
36694
- catch (_) {
36695
- // we don't care about the errors within the evaluation of a rule
37479
+ }
37480
+ break;
36696
37481
  }
36697
37482
  }
36698
37483
  return computedStyle;
@@ -36728,7 +37513,7 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
36728
37513
  return percentile(rangeValues, Number(threshold.value) / 100, true);
36729
37514
  case "formula":
36730
37515
  const value = threshold.value && this.getters.evaluateFormula(sheetId, threshold.value);
36731
- return !(value instanceof Promise) ? value : null;
37516
+ return typeof value === "number" ? value : null;
36732
37517
  default:
36733
37518
  return null;
36734
37519
  }
@@ -37295,7 +38080,7 @@ class AutofillPlugin extends UIPlugin {
37295
38080
  let row = zone.bottom;
37296
38081
  if (col > 0) {
37297
38082
  let leftPosition = { sheetId, col: col - 1, row };
37298
- while (this.getters.getEvaluatedCell(leftPosition).type !== CellValueType.empty ||
38083
+ while (this.getters.getCorrespondingFormulaCell(leftPosition) ||
37299
38084
  this.getters.getCell(leftPosition)?.content) {
37300
38085
  row += 1;
37301
38086
  leftPosition = { sheetId, col: col - 1, row };
@@ -37305,7 +38090,7 @@ class AutofillPlugin extends UIPlugin {
37305
38090
  col = zone.right;
37306
38091
  if (col <= this.getters.getNumberCols(sheetId)) {
37307
38092
  let rightPosition = { sheetId, col: col + 1, row };
37308
- while (this.getters.getEvaluatedCell(rightPosition).type !== CellValueType.empty ||
38093
+ while (this.getters.getCorrespondingFormulaCell(rightPosition) ||
37309
38094
  this.getters.getCell(rightPosition)?.content) {
37310
38095
  row += 1;
37311
38096
  rightPosition = { sheetId, col: col + 1, row };
@@ -47053,7 +47838,7 @@ class Spreadsheet extends owl.Component {
47053
47838
  }
47054
47839
  onKeydown(ev) {
47055
47840
  let keyDownString = "";
47056
- if (ev.ctrlKey || ev.metaKey) {
47841
+ if (isCtrlKey(ev)) {
47057
47842
  keyDownString += "CTRL+";
47058
47843
  }
47059
47844
  keyDownString += ev.key.toUpperCase();
@@ -50812,6 +51597,6 @@ exports.setTranslationMethod = setTranslationMethod;
50812
51597
  exports.tokenize = tokenize;
50813
51598
 
50814
51599
 
50815
- __info__.version = '16.4.16';
50816
- __info__.date = '2023-12-05T09:37:14.210Z';
50817
- __info__.hash = '1abbb92';
51600
+ __info__.version = '16.4.18';
51601
+ __info__.date = '2024-01-12T13:43:10.143Z';
51602
+ __info__.hash = 'f438e08';