@odoo/o-spreadsheet 16.3.18 → 16.3.22

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.
@@ -298,15 +298,80 @@ const FONT_SIZES = [6, 7, 8, 9, 10, 11, 12, 14, 18, 24, 36];
298
298
  // -----------------------------------------------------------------------------
299
299
  // Date Type
300
300
  // -----------------------------------------------------------------------------
301
+ /**
302
+ * A DateTime object that can be used to manipulate spreadsheet dates.
303
+ * Conceptually, a spreadsheet date is simply a number with a date format,
304
+ * and it is timezone-agnostic.
305
+ * This DateTime object consistently uses UTC time to represent a naive date and time.
306
+ */
307
+ class DateTime {
308
+ jsDate;
309
+ constructor(year, month, day, hours = 0, minutes = 0, seconds = 0) {
310
+ this.jsDate = new Date(Date.UTC(year, month, day, hours, minutes, seconds, 0));
311
+ }
312
+ static fromTimestamp(timestamp) {
313
+ const date = new Date(timestamp);
314
+ return new DateTime(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());
315
+ }
316
+ static now() {
317
+ const now = new Date();
318
+ return new DateTime(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds());
319
+ }
320
+ toString() {
321
+ return this.jsDate.toString();
322
+ }
323
+ toLocaleDateString() {
324
+ return this.jsDate.toLocaleDateString();
325
+ }
326
+ getTime() {
327
+ return this.jsDate.getTime();
328
+ }
329
+ getFullYear() {
330
+ return this.jsDate.getUTCFullYear();
331
+ }
332
+ getMonth() {
333
+ return this.jsDate.getUTCMonth();
334
+ }
335
+ getDate() {
336
+ return this.jsDate.getUTCDate();
337
+ }
338
+ getDay() {
339
+ return this.jsDate.getUTCDay();
340
+ }
341
+ getHours() {
342
+ return this.jsDate.getUTCHours();
343
+ }
344
+ getMinutes() {
345
+ return this.jsDate.getUTCMinutes();
346
+ }
347
+ getSeconds() {
348
+ return this.jsDate.getUTCSeconds();
349
+ }
350
+ setFullYear(year) {
351
+ this.jsDate.setFullYear(year);
352
+ }
353
+ setDate(date) {
354
+ this.jsDate.setUTCDate(date);
355
+ }
356
+ setHours(hours) {
357
+ this.jsDate.setUTCHours(hours);
358
+ }
359
+ setMinutes(minutes) {
360
+ this.jsDate.setUTCMinutes(minutes);
361
+ }
362
+ setSeconds(seconds) {
363
+ this.jsDate.setUTCSeconds(seconds);
364
+ }
365
+ }
301
366
  // -----------------------------------------------------------------------------
302
367
  // Parsing
303
368
  // -----------------------------------------------------------------------------
304
- const INITIAL_1900_DAY = new Date(1899, 11, 30);
369
+ const INITIAL_1900_DAY = new DateTime(1899, 11, 30);
305
370
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
306
371
  const CURRENT_MILLENIAL = 2000; // note: don't forget to update this in 2999
307
- const CURRENT_YEAR = new Date().getFullYear();
308
- const INITIAL_JS_DAY = new Date(0);
309
- const DATE_JS_1900_OFFSET = INITIAL_JS_DAY - INITIAL_1900_DAY;
372
+ const CURRENT_YEAR = DateTime.now().getFullYear();
373
+ const INITIAL_JS_DAY = DateTime.fromTimestamp(0);
374
+ const DATE_JS_1900_OFFSET = INITIAL_JS_DAY.getTime() - INITIAL_1900_DAY.getTime();
310
375
  const mdyDateRegexp = /^\d{1,2}(\/|-|\s)\d{1,2}((\/|-|\s)\d{1,4})?$/;
311
376
  const ymdDateRegexp = /^\d{3,4}(\/|-|\s)\d{1,2}(\/|-|\s)\d{1,2}$/;
312
377
  const timeRegexp = /((\d+(:\d+)?(:\d+)?\s*(AM|PM))|(\d+:\d+(:\d+)?))$/;
@@ -355,7 +420,7 @@ function _parseDateTime(str) {
355
420
  return {
356
421
  value: date.value + time.value,
357
422
  format: date.format + " " + (time.format === "hhhh:mm:ss" ? "hh:mm:ss" : time.format),
358
- 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()),
423
+ 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()),
359
424
  };
360
425
  }
361
426
  return date || time;
@@ -372,13 +437,13 @@ function parseDate(str, dateFormat) {
372
437
  const day = Number(parts[dayIndex]);
373
438
  const leadingZero = (parts[monthIndex].length === 2 && month < 10) || (parts[dayIndex].length === 2 && day < 10);
374
439
  const year = parts[yearIndex] ? inferYear(parts[yearIndex]) : CURRENT_YEAR;
375
- const jsDate = new Date(year, month - 1, day);
440
+ const jsDate = new DateTime(year, month - 1, day);
376
441
  const sep = str.match(/\/|-|\s/)[0];
377
442
  if (jsDate.getMonth() !== month - 1 || jsDate.getDate() !== day) {
378
443
  // invalid date
379
444
  return null;
380
445
  }
381
- const delta = jsDate - INITIAL_1900_DAY;
446
+ const delta = jsDate.getTime() - INITIAL_1900_DAY.getTime();
382
447
  let format = leadingZero ? `mm${sep}dd` : `m${sep}d`;
383
448
  if (parts[yearIndex]) {
384
449
  format = isMDY ? format + sep + "yyyy" : "yyyy" + sep + format;
@@ -438,7 +503,7 @@ function parseTime(str) {
438
503
  if (hours >= 24) {
439
504
  format = "hhhh:mm:ss";
440
505
  }
441
- const jsDate = new Date(1899, 11, 30, hours, minutes, seconds);
506
+ const jsDate = new DateTime(1899, 11, 30, hours, minutes, seconds);
442
507
  return {
443
508
  value: hours / 24 + minutes / 1440 + seconds / 86400,
444
509
  format: format,
@@ -452,7 +517,7 @@ function parseTime(str) {
452
517
  // -----------------------------------------------------------------------------
453
518
  function numberToJsDate(value) {
454
519
  const truncValue = Math.trunc(value);
455
- let date = new Date(truncValue * MS_PER_DAY - DATE_JS_1900_OFFSET);
520
+ let date = DateTime.fromTimestamp(truncValue * MS_PER_DAY - DATE_JS_1900_OFFSET);
456
521
  let time = value - truncValue;
457
522
  time = time < 0 ? 1 + time : time;
458
523
  const hours = Math.round(time * 24);
@@ -469,7 +534,7 @@ function jsDateToRoundNumber(date) {
469
534
  }
470
535
  /** Return the number of days in the current month of the given date */
471
536
  function getDaysInMonth(date) {
472
- return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
537
+ return new DateTime(date.getFullYear(), date.getMonth() + 1, 0).getDate();
473
538
  }
474
539
  function isLastDayOfMonth(date) {
475
540
  return getDaysInMonth(date) === date.getDate();
@@ -487,7 +552,7 @@ function addMonthsToDate(date, months, keepEndOfMonth) {
487
552
  const yStart = date.getFullYear();
488
553
  const mStart = date.getMonth();
489
554
  const dStart = date.getDate();
490
- const jsDate = new Date(yStart, mStart + months);
555
+ const jsDate = new DateTime(yStart, mStart + months, 1);
491
556
  if (keepEndOfMonth && dStart === getDaysInMonth(date)) {
492
557
  jsDate.setDate(getDaysInMonth(jsDate));
493
558
  }
@@ -647,13 +712,6 @@ function areTwoDatesWithinOneYear(startDate, endDate) {
647
712
  }
648
713
 
649
714
  //------------------------------------------------------------------------------
650
- /**
651
- * Stringify an object, like JSON.stringify, except that the first level of keys
652
- * is ordered.
653
- */
654
- function stringify(obj) {
655
- return JSON.stringify(obj, Object.keys(obj).sort());
656
- }
657
715
  /**
658
716
  * Remove quotes from a quoted string
659
717
  * ```js
@@ -883,7 +941,7 @@ function isBoolean(str) {
883
941
  function isDateTime(str) {
884
942
  return parseDateTime(str) !== null;
885
943
  }
886
- const MARKDOWN_LINK_REGEX = /^\[([^\[]+)\]\((.+)\)$/;
944
+ const MARKDOWN_LINK_REGEX = /^\[(.+)\]\((.+)\)$/;
887
945
  //link must start with http or https
888
946
  //https://stackoverflow.com/a/3809435/4760614
889
947
  const WEB_LINK_REGEX = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/;
@@ -952,7 +1010,7 @@ function isObjectEmptyRecursive(argument) {
952
1010
  */
953
1011
  function getItemId(item, itemsDic) {
954
1012
  for (let [key, value] of Object.entries(itemsDic)) {
955
- if (stringify(value) === stringify(item)) {
1013
+ if (deepEquals(value, item)) {
956
1014
  return parseInt(key, 10);
957
1015
  }
958
1016
  }
@@ -1952,7 +2010,7 @@ function formatJSTime(jsDate, format) {
1952
2010
  .map((p) => {
1953
2011
  switch (p) {
1954
2012
  case "hhhh":
1955
- const helapsedHours = Math.floor((jsDate.getTime() - INITIAL_1900_DAY) / (60 * 60 * 1000));
2013
+ const helapsedHours = Math.floor((jsDate.getTime() - INITIAL_1900_DAY.getTime()) / (60 * 60 * 1000));
1956
2014
  return helapsedHours.toString();
1957
2015
  case "hh":
1958
2016
  return hours.toString().padStart(2, "0");
@@ -2199,8 +2257,10 @@ function convertInternalFormatToFormat(internalFormat) {
2199
2257
  const cellReference = new RegExp(/\$?([A-Z]{1,3})\$?([0-9]{1,7})/, "i");
2200
2258
  // Same as above, but matches the exact string (nothing before or after)
2201
2259
  const singleCellReference = new RegExp(/^\$?([A-Z]{1,3})\$?([0-9]{1,7})$/, "i");
2202
- /** Reference of a column header (eg. A, AB) */
2203
- const colHeader = new RegExp(/^([A-Z]{1,3})+$/, "i");
2260
+ /** Reference of a column header (eg. A, AB, $A) */
2261
+ const colHeader = new RegExp(/^\$?([A-Z]{1,3})+$/, "i");
2262
+ /** Reference of a row header (eg. 1, $1) */
2263
+ const rowHeader = new RegExp(/^\$?([0-9]{1,7})+$/, "i");
2204
2264
  /** Reference of a column (eg. A, $CA, Sheet1!B) */
2205
2265
  const colReference = new RegExp(/^\s*('.+'!|[^']+!)?\$?([A-Z]{1,3})$/, "i");
2206
2266
  /** Reference of a row (eg. 1, 59, Sheet1!9) */
@@ -2230,6 +2290,9 @@ function isRowReference(xc) {
2230
2290
  function isColHeader(str) {
2231
2291
  return colHeader.test(str);
2232
2292
  }
2293
+ function isRowHeader(str) {
2294
+ return rowHeader.test(str);
2295
+ }
2233
2296
  /**
2234
2297
  * Return true if the given xc is the reference of a single cell,
2235
2298
  * without any specified sheet (e.g. A1)
@@ -2296,18 +2359,10 @@ class RangeImpl {
2296
2359
  if (isFullCol) {
2297
2360
  parts[0].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
2298
2361
  parts[1].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
2299
- if (zone.left === zone.right) {
2300
- parts[0].colFixed = parts[0].colFixed || parts[1].colFixed;
2301
- parts[1].colFixed = parts[0].colFixed || parts[1].colFixed;
2302
- }
2303
2362
  }
2304
2363
  if (isFullRow) {
2305
2364
  parts[0].colFixed = parts[0].colFixed || parts[1].colFixed;
2306
2365
  parts[1].colFixed = parts[0].colFixed || parts[1].colFixed;
2307
- if (zone.top === zone.bottom) {
2308
- parts[0].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
2309
- parts[1].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
2310
- }
2311
2366
  }
2312
2367
  return parts;
2313
2368
  }
@@ -5167,6 +5222,7 @@ const FilterMenuPopoverBuilder = {
5167
5222
  },
5168
5223
  };
5169
5224
 
5225
+ const macRegex = /Mac/i;
5170
5226
  /**
5171
5227
  * Return true if the event was triggered from
5172
5228
  * a child element.
@@ -5212,6 +5268,17 @@ function setElementScrollTop(el, scroll) {
5212
5268
  }
5213
5269
  function getOpenedMenus() {
5214
5270
  return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
5271
+ }
5272
+ function isMacOS() {
5273
+ return Boolean(macRegex.test(navigator.userAgent));
5274
+ }
5275
+ /**
5276
+ * @param {KeyboardEvent | MouseEvent} ev
5277
+ * @returns Returns true if the event was triggered with the "ctrl" modifier pressed.
5278
+ * On Mac, this is the "meta" or "command" key.
5279
+ */
5280
+ function isCtrlKey(ev) {
5281
+ return isMacOS() ? ev.metaKey : ev.ctrlKey;
5215
5282
  }
5216
5283
 
5217
5284
  /**
@@ -9598,6 +9665,9 @@ function arg(definition, description = "") {
9598
9665
  function makeArg(str, description) {
9599
9666
  let parts = str.match(ARG_REGEXP);
9600
9667
  let name = parts[1].trim();
9668
+ if (!name) {
9669
+ throw new Error(`Function argument definition is missing a name: '${str}'.`);
9670
+ }
9601
9671
  let types = [];
9602
9672
  let isOptional = false;
9603
9673
  let isRepeating = false;
@@ -12051,7 +12121,7 @@ const DATE = {
12051
12121
  if (_year < 1900) {
12052
12122
  _year += 1900;
12053
12123
  }
12054
- const jsDate = new Date(_year, _month - 1, _day);
12124
+ const jsDate = new DateTime(_year, _month - 1, _day);
12055
12125
  const result = jsDateToRoundNumber(jsDate);
12056
12126
  assert(() => result >= 0, _lt(`The function [[FUNCTION_NAME]] result must be greater than or equal 01/01/1900.`));
12057
12127
  return result;
@@ -12094,7 +12164,7 @@ const DATEDIF = {
12094
12164
  // See: https://support.microsoft.com/en-us/office/datedif-function-25dba1a4-2812-480b-84dd-8b32a451b35c
12095
12165
  let days = jsEndDate.getDate() - jsStartDate.getDate();
12096
12166
  if (days < 0) {
12097
- const monthBeforeEndMonth = new Date(jsEndDate.getFullYear(), jsEndDate.getMonth() - 1, 1);
12167
+ const monthBeforeEndMonth = new DateTime(jsEndDate.getFullYear(), jsEndDate.getMonth() - 1, 1);
12098
12168
  const daysInMonthBeforeEndMonth = getDaysInMonth(monthBeforeEndMonth);
12099
12169
  days = daysInMonthBeforeEndMonth - Math.abs(days);
12100
12170
  }
@@ -12103,7 +12173,7 @@ const DATEDIF = {
12103
12173
  if (areTwoDatesWithinOneYear(_startDate, _endDate)) {
12104
12174
  return getTimeDifferenceInWholeDays(jsStartDate, jsEndDate);
12105
12175
  }
12106
- const endDateWithinOneYear = new Date(jsStartDate.getFullYear(), jsEndDate.getMonth(), jsEndDate.getDate());
12176
+ const endDateWithinOneYear = new DateTime(jsStartDate.getFullYear(), jsEndDate.getMonth(), jsEndDate.getDate());
12107
12177
  let days = getTimeDifferenceInWholeDays(jsStartDate, endDateWithinOneYear);
12108
12178
  if (days < 0) {
12109
12179
  endDateWithinOneYear.setFullYear(jsStartDate.getFullYear() + 1);
@@ -12216,7 +12286,7 @@ const EOMONTH = {
12216
12286
  const _months = Math.trunc(toNumber(months));
12217
12287
  const yStart = _startDate.getFullYear();
12218
12288
  const mStart = _startDate.getMonth();
12219
- const jsDate = new Date(yStart, mStart + _months + 1, 0);
12289
+ const jsDate = new DateTime(yStart, mStart + _months + 1, 0);
12220
12290
  return jsDateToRoundNumber(jsDate);
12221
12291
  },
12222
12292
  isExported: true,
@@ -12253,17 +12323,17 @@ const ISOWEEKNUM = {
12253
12323
  // The first week of the year is the week that contains the first
12254
12324
  // Thursday of the year.
12255
12325
  let firstThursday = 1;
12256
- while (new Date(y, 0, firstThursday).getDay() !== 4) {
12326
+ while (new DateTime(y, 0, firstThursday).getDay() !== 4) {
12257
12327
  firstThursday += 1;
12258
12328
  }
12259
- const firstDayOfFirstWeek = new Date(y, 0, firstThursday - 3);
12329
+ const firstDayOfFirstWeek = new DateTime(y, 0, firstThursday - 3);
12260
12330
  // The last week of the year is the week that contains the last Thursday of
12261
12331
  // the year.
12262
12332
  let lastThursday = 31;
12263
- while (new Date(y, 11, lastThursday).getDay() !== 4) {
12333
+ while (new DateTime(y, 11, lastThursday).getDay() !== 4) {
12264
12334
  lastThursday -= 1;
12265
12335
  }
12266
- const lastDayOfLastWeek = new Date(y, 11, lastThursday + 3);
12336
+ const lastDayOfLastWeek = new DateTime(y, 11, lastThursday + 3);
12267
12337
  // B - If our date > lastDayOfLastWeek then it's in the weeks of the year after
12268
12338
  // If our date < firstDayOfFirstWeek then it's in the weeks of the year before
12269
12339
  let offsetYear;
@@ -12289,17 +12359,17 @@ const ISOWEEKNUM = {
12289
12359
  case 1:
12290
12360
  // firstDay is the 1st day of the 1st week of the year after
12291
12361
  // firstDay = lastDayOfLastWeek + 1 Day
12292
- firstDay = new Date(y, 11, lastThursday + 3 + 1);
12362
+ firstDay = new DateTime(y, 11, lastThursday + 3 + 1);
12293
12363
  break;
12294
12364
  case -1:
12295
12365
  // firstDay is the 1st day of the 1st week of the previous year.
12296
12366
  // The first week of the previous year is the week that contains the
12297
12367
  // first Thursday of the previous year.
12298
12368
  let firstThursdayPreviousYear = 1;
12299
- while (new Date(y - 1, 0, firstThursdayPreviousYear).getDay() !== 4) {
12369
+ while (new DateTime(y - 1, 0, firstThursdayPreviousYear).getDay() !== 4) {
12300
12370
  firstThursdayPreviousYear += 1;
12301
12371
  }
12302
- firstDay = new Date(y - 1, 0, firstThursdayPreviousYear - 3);
12372
+ firstDay = new DateTime(y - 1, 0, firstThursdayPreviousYear - 3);
12303
12373
  break;
12304
12374
  }
12305
12375
  const diff = (_date.getTime() - firstDay.getTime()) / MS_PER_DAY;
@@ -12434,8 +12504,8 @@ const NETWORKDAYS_INTL = {
12434
12504
  });
12435
12505
  }
12436
12506
  const invertDate = _startDate.getTime() > _endDate.getTime();
12437
- const stopDate = new Date((invertDate ? _startDate : _endDate).getTime());
12438
- let stepDate = new Date((invertDate ? _endDate : _startDate).getTime());
12507
+ const stopDate = DateTime.fromTimestamp((invertDate ? _startDate : _endDate).getTime());
12508
+ let stepDate = DateTime.fromTimestamp((invertDate ? _endDate : _startDate).getTime());
12439
12509
  const timeStopDate = stopDate.getTime();
12440
12510
  let timeStepDate = stepDate.getTime();
12441
12511
  let netWorkingDay = 0;
@@ -12459,8 +12529,7 @@ const NOW = {
12459
12529
  returns: ["DATE"],
12460
12530
  computeFormat: () => "m/d/yyyy hh:mm:ss",
12461
12531
  compute: function () {
12462
- let today = new Date();
12463
- today.setMilliseconds(0);
12532
+ let today = DateTime.now();
12464
12533
  const delta = today.getTime() - INITIAL_1900_DAY.getTime();
12465
12534
  const time = today.getHours() / 24 + today.getMinutes() / 1440 + today.getSeconds() / 86400;
12466
12535
  return Math.floor(delta / MS_PER_DAY) + time;
@@ -12530,8 +12599,8 @@ const TODAY = {
12530
12599
  returns: ["DATE"],
12531
12600
  computeFormat: () => "m/d/yyyy",
12532
12601
  compute: function () {
12533
- const today = new Date();
12534
- const jsDate = new Date(today.getFullYear(), today.getMonth(), today.getDate());
12602
+ const today = DateTime.now();
12603
+ const jsDate = new DateTime(today.getFullYear(), today.getMonth(), today.getDate());
12535
12604
  return jsDateToRoundNumber(jsDate);
12536
12605
  },
12537
12606
  isExported: true,
@@ -12586,10 +12655,10 @@ const WEEKNUM = {
12586
12655
  }
12587
12656
  const y = _date.getFullYear();
12588
12657
  let dayStart = 1;
12589
- let startDayOfFirstWeek = new Date(y, 0, dayStart);
12658
+ let startDayOfFirstWeek = new DateTime(y, 0, dayStart);
12590
12659
  while (startDayOfFirstWeek.getDay() !== startDayOfWeek) {
12591
12660
  dayStart += 1;
12592
- startDayOfFirstWeek = new Date(y, 0, dayStart);
12661
+ startDayOfFirstWeek = new DateTime(y, 0, dayStart);
12593
12662
  }
12594
12663
  const dif = (_date.getTime() - startDayOfFirstWeek.getTime()) / MS_PER_DAY;
12595
12664
  if (dif < 0) {
@@ -12643,7 +12712,7 @@ const WORKDAY_INTL = {
12643
12712
  timesHoliday.add(holiday.getTime());
12644
12713
  });
12645
12714
  }
12646
- let stepDate = new Date(_startDate.getTime());
12715
+ let stepDate = DateTime.fromTimestamp(_startDate.getTime());
12647
12716
  let timeStepDate = stepDate.getTime();
12648
12717
  const unitDay = Math.sign(_numDays);
12649
12718
  let stepDay = Math.abs(_numDays);
@@ -12705,7 +12774,7 @@ const MONTH_START = {
12705
12774
  const _startDate = toJsDate(date);
12706
12775
  const yStart = _startDate.getFullYear();
12707
12776
  const mStart = _startDate.getMonth();
12708
- const jsDate = new Date(yStart, mStart, 1);
12777
+ const jsDate = new DateTime(yStart, mStart, 1);
12709
12778
  return jsDateToRoundNumber(jsDate);
12710
12779
  },
12711
12780
  };
@@ -12743,7 +12812,7 @@ const QUARTER_START = {
12743
12812
  compute: function (date) {
12744
12813
  const quarter = QUARTER.compute(date);
12745
12814
  const year = YEAR.compute(date);
12746
- const jsDate = new Date(year, (quarter - 1) * 3, 1);
12815
+ const jsDate = new DateTime(year, (quarter - 1) * 3, 1);
12747
12816
  return jsDateToRoundNumber(jsDate);
12748
12817
  },
12749
12818
  };
@@ -12758,7 +12827,7 @@ const QUARTER_END = {
12758
12827
  compute: function (date) {
12759
12828
  const quarter = QUARTER.compute(date);
12760
12829
  const year = YEAR.compute(date);
12761
- const jsDate = new Date(year, quarter * 3, 0);
12830
+ const jsDate = new DateTime(year, quarter * 3, 0);
12762
12831
  return jsDateToRoundNumber(jsDate);
12763
12832
  },
12764
12833
  };
@@ -12772,7 +12841,7 @@ const YEAR_START = {
12772
12841
  computeFormat: () => "m/d/yyyy",
12773
12842
  compute: function (date) {
12774
12843
  const year = YEAR.compute(date);
12775
- const jsDate = new Date(year, 0, 1);
12844
+ const jsDate = new DateTime(year, 0, 1);
12776
12845
  return jsDateToRoundNumber(jsDate);
12777
12846
  },
12778
12847
  };
@@ -12786,7 +12855,7 @@ const YEAR_END = {
12786
12855
  computeFormat: () => "m/d/yyyy",
12787
12856
  compute: function (date) {
12788
12857
  const year = YEAR.compute(date);
12789
- const jsDate = new Date(year + 1, 0, 0);
12858
+ const jsDate = new DateTime(year + 1, 0, 0);
12790
12859
  return jsDateToRoundNumber(jsDate);
12791
12860
  },
12792
12861
  };
@@ -12834,8 +12903,8 @@ const DEFAULT_DELTA_ARG = 0;
12834
12903
  const DELTA = {
12835
12904
  description: _lt("Compare two numeric values, returning 1 if they're equal."),
12836
12905
  args: [
12837
- arg(" (number)", _lt("The first number to compare.")),
12838
- arg(` (number, default=${DEFAULT_DELTA_ARG})`, _lt("The second number to compare.")),
12906
+ arg("number1 (number)", _lt("The first number to compare.")),
12907
+ arg(`number2 (number, default=${DEFAULT_DELTA_ARG})`, _lt("The second number to compare.")),
12839
12908
  ],
12840
12909
  returns: ["NUMBER"],
12841
12910
  compute: function (number1, number2 = DEFAULT_DELTA_ARG) {
@@ -12921,7 +12990,7 @@ function assertDeprecationFactorStrictlyPositive(factor) {
12921
12990
  function assertSettlementLessThanOneYearBeforeMaturity(settlement, maturity) {
12922
12991
  const startDate = toJsDate(settlement);
12923
12992
  const endDate = toJsDate(maturity);
12924
- const startDatePlusOneYear = new Date(startDate);
12993
+ const startDatePlusOneYear = toJsDate(settlement);
12925
12994
  startDatePlusOneYear.setFullYear(startDate.getFullYear() + 1);
12926
12995
  assert(() => endDate.getTime() <= startDatePlusOneYear.getTime(), _lt("The settlement date (%s) must at most one year after the maturity date (%s).", settlement.toString(), maturity.toString()));
12927
12996
  }
@@ -13056,7 +13125,7 @@ const AMORLINC = {
13056
13125
  arg("salvage (number)", _lt("The value of the asset at the end of depreciation.")),
13057
13126
  arg("period (number)", _lt("The single period within life for which to calculate depreciation.")),
13058
13127
  arg("rate (number)", _lt("The deprecation rate.")),
13059
- arg(" (number, optional)", _lt("An indicator of what day count method to use.")),
13128
+ arg("day_count_convention (number, optional)", _lt("An indicator of what day count method to use.")),
13060
13129
  ],
13061
13130
  returns: ["NUMBER"],
13062
13131
  compute: function (cost, purchaseDate, firstPeriodEnd, salvage, period, rate, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
@@ -16039,8 +16108,8 @@ const MID = {
16039
16108
  description: _lt("A segment of a string."),
16040
16109
  args: [
16041
16110
  arg("text (string)", _lt("The string to extract a segment from.")),
16042
- arg(" (number)", _lt("The index from the left of string from which to begin extracting. The first character in string has the index 1.")),
16043
- arg(" (number)", _lt("The length of the segment to extract.")),
16111
+ 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.")),
16112
+ arg("extract_length (number)", _lt("The length of the segment to extract.")),
16044
16113
  ],
16045
16114
  returns: ["STRING"],
16046
16115
  compute: function (text, starting_at, extract_length) {
@@ -17709,10 +17778,10 @@ const arrowMap = {
17709
17778
  function updateSelectionWithArrowKeys(ev, selection) {
17710
17779
  const direction = arrowMap[ev.key];
17711
17780
  if (ev.shiftKey) {
17712
- selection.resizeAnchorZone(direction, ev.ctrlKey ? "end" : 1);
17781
+ selection.resizeAnchorZone(direction, isCtrlKey(ev) ? "end" : 1);
17713
17782
  }
17714
17783
  else {
17715
- selection.moveAnchorCell(direction, ev.ctrlKey ? "end" : 1);
17784
+ selection.moveAnchorCell(direction, isCtrlKey(ev) ? "end" : 1);
17716
17785
  }
17717
17786
  }
17718
17787
 
@@ -22409,7 +22478,10 @@ class GridOverlay extends Component {
22409
22478
  return;
22410
22479
  }
22411
22480
  const [col, row] = this.getCartesianCoordinates(ev);
22412
- this.props.onCellClicked(col, row, { shiftKey: ev.shiftKey, ctrlKey: ev.ctrlKey });
22481
+ this.props.onCellClicked(col, row, {
22482
+ expandZone: ev.shiftKey,
22483
+ addZone: isCtrlKey(ev),
22484
+ });
22413
22485
  }
22414
22486
  onDoubleClick(ev) {
22415
22487
  const [col, row] = this.getCartesianCoordinates(ev);
@@ -22482,6 +22554,7 @@ class AbstractResizer extends Component {
22482
22554
  draggerShadowThickness: 0,
22483
22555
  delta: 0,
22484
22556
  base: 0,
22557
+ position: "before",
22485
22558
  });
22486
22559
  _computeHandleDisplay(ev) {
22487
22560
  const position = this._getEvOffset(ev);
@@ -22606,11 +22679,13 @@ class AbstractResizer extends Component {
22606
22679
  this.state.draggerLinePosition = dimensions.start;
22607
22680
  this.state.draggerShadowPosition = dimensions.start;
22608
22681
  this.state.base = elementIndex;
22682
+ this.state.position = "before";
22609
22683
  }
22610
22684
  else if (this._getSelectedZoneEnd() < elementIndex) {
22611
22685
  this.state.draggerLinePosition = dimensions.end;
22612
22686
  this.state.draggerShadowPosition = dimensions.end - this.state.draggerShadowThickness;
22613
- this.state.base = elementIndex + 1;
22687
+ this.state.base = elementIndex;
22688
+ this.state.position = "after";
22614
22689
  }
22615
22690
  else {
22616
22691
  this.state.draggerLinePosition = startDimensions.start;
@@ -22634,7 +22709,7 @@ class AbstractResizer extends Component {
22634
22709
  this._increaseSelection(index);
22635
22710
  }
22636
22711
  else {
22637
- this._selectElement(index, ev.ctrlKey);
22712
+ this._selectElement(index, isCtrlKey(ev));
22638
22713
  }
22639
22714
  this.lastSelectedElementIndex = index;
22640
22715
  const mouseMoveSelect = (col, row) => {
@@ -22647,7 +22722,7 @@ class AbstractResizer extends Component {
22647
22722
  const mouseUpSelect = () => {
22648
22723
  this.state.isSelecting = false;
22649
22724
  this.lastSelectedElementIndex = null;
22650
- this.env.model.dispatch(ev.ctrlKey ? "PREPARE_SELECTION_INPUT_EXPANSION" : "STOP_SELECTION_INPUT");
22725
+ this.env.model.dispatch(isCtrlKey(ev) ? "PREPARE_SELECTION_INPUT_EXPANSION" : "STOP_SELECTION_INPUT");
22651
22726
  this._computeGrabDisplay(ev);
22652
22727
  };
22653
22728
  dragAndDropBeyondTheViewport(this.env, mouseMoveSelect, mouseUpSelect);
@@ -22793,13 +22868,14 @@ class ColResizer extends AbstractResizer {
22793
22868
  dimension: "COL",
22794
22869
  base: this.state.base,
22795
22870
  elements,
22871
+ position: this.state.position,
22796
22872
  });
22797
22873
  if (!result.isSuccessful && result.reasons.includes(2 /* CommandResult.WillRemoveExistingMerge */)) {
22798
22874
  this.env.raiseError(MergeErrorMessage);
22799
22875
  }
22800
22876
  }
22801
- _selectElement(index, ctrlKey) {
22802
- this.env.model.selection.selectColumn(index, ctrlKey ? "newAnchor" : "overrideSelection");
22877
+ _selectElement(index, addDistinctHeader) {
22878
+ this.env.model.selection.selectColumn(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
22803
22879
  }
22804
22880
  _increaseSelection(index) {
22805
22881
  this.env.model.selection.selectColumn(index, "updateAnchor");
@@ -22966,13 +23042,14 @@ class RowResizer extends AbstractResizer {
22966
23042
  dimension: "ROW",
22967
23043
  base: this.state.base,
22968
23044
  elements,
23045
+ position: this.state.position,
22969
23046
  });
22970
23047
  if (!result.isSuccessful && result.reasons.includes(2 /* CommandResult.WillRemoveExistingMerge */)) {
22971
23048
  this.env.raiseError(MergeErrorMessage);
22972
23049
  }
22973
23050
  }
22974
- _selectElement(index, ctrlKey) {
22975
- this.env.model.selection.selectRow(index, ctrlKey ? "newAnchor" : "overrideSelection");
23051
+ _selectElement(index, addDistinctHeader) {
23052
+ this.env.model.selection.selectRow(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
22976
23053
  }
22977
23054
  _increaseSelection(index) {
22978
23055
  this.env.model.selection.selectRow(index, "updateAnchor");
@@ -23073,8 +23150,8 @@ function useWheelHandler(handler) {
23073
23150
  return val * (deltaMode === 0 ? 1 : DEFAULT_CELL_HEIGHT);
23074
23151
  }
23075
23152
  const onMouseWheel = (ev) => {
23076
- const deltaX = normalize(ev.shiftKey ? ev.deltaY : ev.deltaX, ev.deltaMode);
23077
- const deltaY = normalize(ev.shiftKey ? ev.deltaX : ev.deltaY, ev.deltaMode);
23153
+ const deltaX = normalize(ev.shiftKey && !isMacOS() ? ev.deltaY : ev.deltaX, ev.deltaMode);
23154
+ const deltaY = normalize(ev.shiftKey && !isMacOS() ? ev.deltaX : ev.deltaY, ev.deltaMode);
23078
23155
  handler(deltaX, deltaY);
23079
23156
  };
23080
23157
  return onMouseWheel;
@@ -23776,8 +23853,8 @@ class Grid extends Component {
23776
23853
  // ---------------------------------------------------------------------------
23777
23854
  // Zone selection with mouse
23778
23855
  // ---------------------------------------------------------------------------
23779
- onCellClicked(col, row, { ctrlKey, shiftKey }) {
23780
- if (ctrlKey) {
23856
+ onCellClicked(col, row, { addZone, expandZone }) {
23857
+ if (addZone) {
23781
23858
  this.env.model.dispatch("PREPARE_SELECTION_INPUT_EXPANSION");
23782
23859
  }
23783
23860
  if (this.env.model.getters.hasOpenedPopover()) {
@@ -23786,10 +23863,10 @@ class Grid extends Component {
23786
23863
  if (this.env.model.getters.getEditionMode() === "editing") {
23787
23864
  this.env.model.dispatch("STOP_EDITION");
23788
23865
  }
23789
- if (shiftKey) {
23866
+ if (expandZone) {
23790
23867
  this.env.model.selection.setAnchorCorner(col, row);
23791
23868
  }
23792
- else if (ctrlKey) {
23869
+ else if (addZone) {
23793
23870
  this.env.model.selection.addCellToSelection(col, row);
23794
23871
  }
23795
23872
  else {
@@ -23851,9 +23928,7 @@ class Grid extends Component {
23851
23928
  return;
23852
23929
  }
23853
23930
  let keyDownString = "";
23854
- if (ev.ctrlKey)
23855
- keyDownString += "CTRL+";
23856
- if (ev.metaKey)
23931
+ if (isCtrlKey(ev))
23857
23932
  keyDownString += "CTRL+";
23858
23933
  if (ev.altKey)
23859
23934
  keyDownString += "ALT+";
@@ -25761,7 +25836,7 @@ const machine = {
25761
25836
  SPACE: goTo(State.RightRef),
25762
25837
  NUMBER: goTo(State.Found),
25763
25838
  REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
25764
- SYMBOL: goTo(State.Found, (token) => isColHeader(token.value)),
25839
+ SYMBOL: goTo(State.Found, (token) => isColHeader(token.value) || isRowHeader(token.value)),
25765
25840
  },
25766
25841
  [State.RightColumnRef]: {
25767
25842
  SPACE: goTo(State.RightColumnRef),
@@ -25772,6 +25847,7 @@ const machine = {
25772
25847
  SPACE: goTo(State.RightRowRef),
25773
25848
  NUMBER: goTo(State.Found),
25774
25849
  REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
25850
+ SYMBOL: goTo(State.Found, (token) => isRowHeader(token.value)),
25775
25851
  },
25776
25852
  [State.Found]: {},
25777
25853
  };
@@ -29479,7 +29555,7 @@ class BordersPlugin extends CorePlugin {
29479
29555
  */
29480
29556
  function getBorderId(border) {
29481
29557
  for (let [key, value] of Object.entries(borders)) {
29482
- if (stringify(value) === stringify(border)) {
29558
+ if (deepEquals(value, border)) {
29483
29559
  return parseInt(key, 10);
29484
29560
  }
29485
29561
  }
@@ -32447,7 +32523,7 @@ class SheetPlugin extends CorePlugin {
32447
32523
  const elements = cmd.dimension === "COL"
32448
32524
  ? this.getNumberCols(cmd.sheetId)
32449
32525
  : this.getNumberRows(cmd.sheetId);
32450
- if (cmd.base < 0 || cmd.base > elements) {
32526
+ if (cmd.base < 0 || cmd.base >= elements) {
32451
32527
  return 86 /* CommandResult.InvalidHeaderIndex */;
32452
32528
  }
32453
32529
  else if (cmd.quantity <= 0) {
@@ -33480,13 +33556,18 @@ class EvaluationPlugin extends UIPlugin {
33480
33556
  // Getters
33481
33557
  // ---------------------------------------------------------------------------
33482
33558
  evaluateFormula(formulaString, sheetId = this.getters.getActiveSheetId()) {
33483
- const compiledFormula = compile(formulaString);
33484
- const params = this.getCompilationParameters((cell) => this.getEvaluatedCell(this.getters.getCellPosition(cell.id)));
33485
- const ranges = [];
33486
- for (let xc of compiledFormula.dependencies) {
33487
- ranges.push(this.getters.getRangeFromSheetXC(sheetId, xc));
33559
+ try {
33560
+ const compiledFormula = compile(formulaString);
33561
+ const params = this.getCompilationParameters((cell) => this.getEvaluatedCell(this.getters.getCellPosition(cell.id)));
33562
+ const ranges = [];
33563
+ for (let xc of compiledFormula.dependencies) {
33564
+ ranges.push(this.getters.getRangeFromSheetXC(sheetId, xc));
33565
+ }
33566
+ return compiledFormula.execute(ranges, ...params).value;
33567
+ }
33568
+ catch (error) {
33569
+ return error instanceof EvaluationError ? error.errorType : CellErrorType.GenericError;
33488
33570
  }
33489
- return compiledFormula.execute(ranges, ...params).value;
33490
33571
  }
33491
33572
  /**
33492
33573
  * Return the value of each cell in the range as they are displayed in the grid.
@@ -33911,39 +33992,34 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
33911
33992
  this.computedIcons[sheetId] = {};
33912
33993
  const computedStyle = this.computedStyles[sheetId];
33913
33994
  for (let cf of this.getters.getConditionalFormats(sheetId).reverse()) {
33914
- try {
33915
- switch (cf.rule.type) {
33916
- case "ColorScaleRule":
33917
- for (let range of cf.ranges) {
33918
- this.applyColorScale(range, cf.rule);
33919
- }
33920
- break;
33921
- case "IconSetRule":
33922
- for (let range of cf.ranges) {
33923
- this.applyIcon(range, cf.rule);
33924
- }
33925
- break;
33926
- default:
33927
- for (let ref of cf.ranges) {
33928
- const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
33929
- for (let row = zone.top; row <= zone.bottom; row++) {
33930
- for (let col = zone.left; col <= zone.right; col++) {
33931
- const pr = this.rulePredicate[cf.rule.type];
33932
- let cell = this.getters.getEvaluatedCell({ sheetId, col, row });
33933
- if (pr && pr(cell, cf.rule)) {
33934
- if (!computedStyle[col])
33935
- computedStyle[col] = [];
33936
- // we must combine all the properties of all the CF rules applied to the given cell
33937
- computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
33938
- }
33995
+ switch (cf.rule.type) {
33996
+ case "ColorScaleRule":
33997
+ for (let range of cf.ranges) {
33998
+ this.applyColorScale(range, cf.rule);
33999
+ }
34000
+ break;
34001
+ case "IconSetRule":
34002
+ for (let range of cf.ranges) {
34003
+ this.applyIcon(range, cf.rule);
34004
+ }
34005
+ break;
34006
+ default:
34007
+ for (let ref of cf.ranges) {
34008
+ const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
34009
+ for (let row = zone.top; row <= zone.bottom; row++) {
34010
+ for (let col = zone.left; col <= zone.right; col++) {
34011
+ const pr = this.rulePredicate[cf.rule.type];
34012
+ let cell = this.getters.getEvaluatedCell({ sheetId, col, row });
34013
+ if (pr && pr(cell, cf.rule)) {
34014
+ if (!computedStyle[col])
34015
+ computedStyle[col] = [];
34016
+ // we must combine all the properties of all the CF rules applied to the given cell
34017
+ computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
33939
34018
  }
33940
34019
  }
33941
34020
  }
33942
- break;
33943
- }
33944
- }
33945
- catch (_) {
33946
- // we don't care about the errors within the evaluation of a rule
34021
+ }
34022
+ break;
33947
34023
  }
33948
34024
  }
33949
34025
  }
@@ -33968,7 +34044,7 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
33968
34044
  return percentile(rangeValues, Number(threshold.value) / 100, true);
33969
34045
  case "formula":
33970
34046
  const value = threshold.value && this.getters.evaluateFormula(threshold.value);
33971
- return !(value instanceof Promise) ? value : null;
34047
+ return typeof value === "number" ? value : null;
33972
34048
  default:
33973
34049
  return null;
33974
34050
  }
@@ -35617,19 +35693,21 @@ class AutofillPlugin extends UIPlugin {
35617
35693
  let col = zone.left;
35618
35694
  let row = zone.bottom;
35619
35695
  if (col > 0) {
35620
- let left = this.getters.getEvaluatedCell({ sheetId, col: col - 1, row });
35621
- while (left.type !== CellValueType.empty) {
35696
+ let leftPosition = { sheetId, col: col - 1, row };
35697
+ while (this.getters.getEvaluatedCell(leftPosition).type !== CellValueType.empty ||
35698
+ this.getters.getCell(leftPosition)?.content) {
35622
35699
  row += 1;
35623
- left = this.getters.getEvaluatedCell({ sheetId, col: col - 1, row });
35700
+ leftPosition = { sheetId, col: col - 1, row };
35624
35701
  }
35625
35702
  }
35626
35703
  if (row === zone.bottom) {
35627
35704
  col = zone.right;
35628
35705
  if (col <= this.getters.getNumberCols(sheetId)) {
35629
- let right = this.getters.getEvaluatedCell({ sheetId, col: col + 1, row });
35630
- while (right.type !== CellValueType.empty) {
35706
+ let rightPosition = { sheetId, col: col + 1, row };
35707
+ while (this.getters.getEvaluatedCell(rightPosition).type !== CellValueType.empty ||
35708
+ this.getters.getCell(rightPosition)?.content) {
35631
35709
  row += 1;
35632
- right = this.getters.getEvaluatedCell({ sheetId, col: col + 1, row });
35710
+ rightPosition = { sheetId, col: col + 1, row };
35633
35711
  }
35634
35712
  }
35635
35713
  }
@@ -40577,6 +40655,10 @@ function setXcToReferenceType(xc, referenceType) {
40577
40655
  return xc.slice(0, indexOfNumber) + "$" + xc.slice(indexOfNumber);
40578
40656
  case "colrow":
40579
40657
  indexOfNumber = xc.search(/[0-9]/);
40658
+ if (indexOfNumber === -1 || indexOfNumber === 0) {
40659
+ // no row number (eg. A) or no column (eg. 1)
40660
+ return "$" + xc;
40661
+ }
40580
40662
  xc = xc.slice(0, indexOfNumber) + "$" + xc.slice(indexOfNumber);
40581
40663
  return "$" + xc;
40582
40664
  case "none":
@@ -41758,7 +41840,7 @@ class GridSelectionPlugin extends UIPlugin {
41758
41840
  sheetId: cmd.sheetId,
41759
41841
  base: cmd.base,
41760
41842
  quantity: thickness,
41761
- position: "before",
41843
+ position: cmd.position,
41762
41844
  });
41763
41845
  const isCol = cmd.dimension === "COL";
41764
41846
  const start = cmd.elements[0];
@@ -41775,12 +41857,13 @@ class GridSelectionPlugin extends UIPlugin {
41775
41857
  },
41776
41858
  ];
41777
41859
  const state = new ClipboardCellsState(target, "CUT", this.getters, this.dispatch, this.selection);
41860
+ const base = isBasedBefore ? cmd.base : cmd.base + 1;
41778
41861
  const pasteTarget = [
41779
41862
  {
41780
- left: isCol ? cmd.base : 0,
41781
- right: isCol ? cmd.base + thickness - 1 : this.getters.getNumberCols(cmd.sheetId) - 1,
41782
- top: !isCol ? cmd.base : 0,
41783
- bottom: !isCol ? cmd.base + thickness - 1 : this.getters.getNumberRows(cmd.sheetId) - 1,
41863
+ left: isCol ? base : 0,
41864
+ right: isCol ? base + thickness - 1 : this.getters.getNumberCols(cmd.sheetId) - 1,
41865
+ top: !isCol ? base : 0,
41866
+ bottom: !isCol ? base + thickness - 1 : this.getters.getNumberRows(cmd.sheetId) - 1,
41784
41867
  },
41785
41868
  ];
41786
41869
  state.paste(pasteTarget, { selectTarget: true });
@@ -41817,6 +41900,11 @@ class GridSelectionPlugin extends UIPlugin {
41817
41900
  doesElementsHaveCommonMerges(id, cmd.base - 1, cmd.base)) {
41818
41901
  return 2 /* CommandResult.WillRemoveExistingMerge */;
41819
41902
  }
41903
+ const headers = [cmd.base, ...cmd.elements];
41904
+ const maxHeaderValue = isCol ? this.getters.getNumberCols(id) : this.getters.getNumberRows(id);
41905
+ if (headers.some((h) => h < 0 || h >= maxHeaderValue)) {
41906
+ return 86 /* CommandResult.InvalidHeaderIndex */;
41907
+ }
41820
41908
  return 0 /* CommandResult.Success */;
41821
41909
  }
41822
41910
  //-------------------------------------------
@@ -44098,7 +44186,7 @@ class Spreadsheet extends Component {
44098
44186
  }
44099
44187
  onKeydown(ev) {
44100
44188
  let keyDownString = "";
44101
- if (ev.ctrlKey || ev.metaKey) {
44189
+ if (isCtrlKey(ev)) {
44102
44190
  keyDownString += "CTRL+";
44103
44191
  }
44104
44192
  keyDownString += ev.key.toUpperCase();
@@ -47807,6 +47895,6 @@ function addFunction(functionName, functionDescription) {
47807
47895
  export { AbstractChart, CommandResult, CorePlugin, DispatchResult, EvaluationError, Model, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, UIPlugin, __info__, addFunction, astToFormula, compile, components, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, invalidateEvaluationCommands, links, load, parse, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, tokenize };
47808
47896
 
47809
47897
 
47810
- __info__.version = '16.3.18';
47811
- __info__.date = '2023-11-24T13:10:15.278Z';
47812
- __info__.hash = '45f745e';
47898
+ __info__.version = '16.3.22';
47899
+ __info__.date = '2024-01-17T10:24:07.652Z';
47900
+ __info__.hash = '2c1b136';