@odoo/o-spreadsheet 19.2.24 → 19.2.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /*
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 19.2.24
6
- * @date 2026-08-10T12:32:28.468Z
7
- * @hash 31fa530
5
+ * @version 19.2.26
6
+ * @date 2026-08-21T15:30:16.234Z
7
+ * @hash b9d68c1
8
8
  */
9
9
  :root {
10
10
  --os-gray-100: light-dark(#f9fafb, #1b1d26);
@@ -291,6 +291,7 @@
291
291
  .os-input {
292
292
  border-width: 0 0 1px 0;
293
293
  border-color: transparent;
294
+ border-style: solid;
294
295
  outline: none;
295
296
  text-overflow: ellipsis;
296
297
  color: var(--os-text-body);
@@ -571,6 +572,7 @@
571
572
  width: 100%;
572
573
  outline: none;
573
574
  border-color: var(--os-border-color);
575
+ border-style: solid;
574
576
  color: var(--os-gray-900);
575
577
 
576
578
  &::placeholder {
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 19.2.24
6
- * @date 2026-08-10T12:32:26.974Z
7
- * @hash 31fa530
5
+ * @version 19.2.26
6
+ * @date 2026-08-21T15:30:14.701Z
7
+ * @hash b9d68c1
8
8
  */
9
9
 
10
10
  import { App, Component, blockDom, markRaw, onMounted, onPatched, onWillPatch, onWillStart, onWillUnmount, onWillUpdateProps, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, whenReady, xml } from "@odoo/owl";
@@ -3751,6 +3751,18 @@ function toString(data) {
3751
3751
  default: return "";
3752
3752
  }
3753
3753
  }
3754
+ /**
3755
+ * Only converts the decimal separator, ignore number format such as % or dates
3756
+ */
3757
+ function toLocaleString(data, locale) {
3758
+ const value = toValue(data);
3759
+ switch (typeof value) {
3760
+ case "string": return value;
3761
+ case "number": return value.toString().replace(".", locale.decimalSeparator);
3762
+ case "boolean": return value ? "TRUE" : "FALSE";
3763
+ default: return "";
3764
+ }
3765
+ }
3754
3766
  /** Normalize string by setting it to lowercase and replacing accent letters with plain letters */
3755
3767
  const normalizeString = memoize(function normalizeString(str) {
3756
3768
  return str.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
@@ -18291,7 +18303,7 @@ const ARRAYTOTEXT = {
18291
18303
  else if (_format === 0) {
18292
18304
  const rowSeparator = this.locale.decimalSeparator === "," ? "/" : ",";
18293
18305
  return transposeMatrix(_array).flatMap((row) => row.map((value) => {
18294
- return isEvaluationError(value.value) ? value.value : toString(value);
18306
+ return isEvaluationError(value.value) ? value.value : toLocaleString(value, this.locale);
18295
18307
  })).join(rowSeparator);
18296
18308
  } else return new EvaluationError(_t("Format must be 0 or 1"));
18297
18309
  },
@@ -24334,7 +24346,7 @@ const CONCAT = {
24334
24346
  description: _t("Concatenation of two values."),
24335
24347
  args: [arg("value1 (string)", _t("The value to which value2 will be appended.")), arg("value2 (string)", _t("The value to append to value1."))],
24336
24348
  compute: function(value1, value2) {
24337
- return toString(value1) + toString(value2);
24349
+ return toLocaleString(value1, this.locale) + toLocaleString(value2, this.locale);
24338
24350
  },
24339
24351
  isExported: true
24340
24352
  };
@@ -25203,7 +25215,7 @@ const CLEAN = {
25203
25215
  description: _t("Remove non-printable characters from a piece of text."),
25204
25216
  args: [arg("text (string)", _t("The text whose non-printable characters are to be removed."))],
25205
25217
  compute: function(text) {
25206
- const _text = toString(text);
25218
+ const _text = toLocaleString(text, this.locale);
25207
25219
  let cleanedStr = "";
25208
25220
  for (const char of _text) if (char && char.charCodeAt(0) > 31) cleanedStr += char;
25209
25221
  return cleanedStr;
@@ -25214,7 +25226,7 @@ const CONCATENATE = {
25214
25226
  description: _t("Appends strings to one another."),
25215
25227
  args: [arg("string (string, range<string>, repeating)", _t("String to append in sequence."))],
25216
25228
  compute: function(...datas) {
25217
- return reduceAny(datas, (acc, a) => acc + toString(a), "");
25229
+ return reduceAny(datas, (acc, a) => acc + toLocaleString(a, this.locale), "");
25218
25230
  },
25219
25231
  isExported: true
25220
25232
  };
@@ -25222,7 +25234,7 @@ const EXACT = {
25222
25234
  description: _t("Tests whether two strings are identical."),
25223
25235
  args: [arg("string1 (string)", _t("The first string to compare.")), arg("string2 (string)", _t("The second string to compare."))],
25224
25236
  compute: function(string1, string2) {
25225
- return toString(string1) === toString(string2);
25237
+ return toLocaleString(string1, this.locale) === toLocaleString(string2, this.locale);
25226
25238
  },
25227
25239
  isExported: true
25228
25240
  };
@@ -25234,8 +25246,8 @@ const FIND = {
25234
25246
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
25235
25247
  ],
25236
25248
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
25237
- const _searchFor = toString(searchFor);
25238
- const _textToSearch = toString(textToSearch);
25249
+ const _searchFor = toLocaleString(searchFor, this.locale);
25250
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
25239
25251
  const _startingAt = toNumber(startingAt, this.locale);
25240
25252
  if (_textToSearch === "") return new EvaluationError(_t("The text_to_search must be non-empty."));
25241
25253
  if (_startingAt < 1) return new EvaluationError(_t("The starting_at (%s) must be greater than or equal to 1.", _startingAt));
@@ -25272,8 +25284,8 @@ const JOIN = {
25272
25284
  description: _t("Concatenates elements of arrays with delimiter."),
25273
25285
  args: [arg("delimiter (string)", _t("The character or string to place between each concatenated value.")), arg("value_or_array (string, range<string>, repeating)", _t("Value to be appended using delimiter."))],
25274
25286
  compute: function(delimiter, ...valuesOrArrays) {
25275
- const _delimiter = toString(delimiter);
25276
- return reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toString(a), "");
25287
+ const _delimiter = toLocaleString(delimiter, this.locale);
25288
+ return reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toLocaleString(a, this.locale), "");
25277
25289
  }
25278
25290
  };
25279
25291
  const LEFT = {
@@ -25282,7 +25294,7 @@ const LEFT = {
25282
25294
  compute: function(text, ...args) {
25283
25295
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
25284
25296
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters));
25285
- return toString(text).substring(0, _numberOfCharacters);
25297
+ return toLocaleString(text, this.locale).substring(0, _numberOfCharacters);
25286
25298
  },
25287
25299
  isExported: true
25288
25300
  };
@@ -25290,7 +25302,7 @@ const LEN = {
25290
25302
  description: _t("Length of a string."),
25291
25303
  args: [arg("text (string)", _t("The string whose length will be returned."))],
25292
25304
  compute: function(text) {
25293
- return toString(text).length;
25305
+ return toLocaleString(text, this.locale).length;
25294
25306
  },
25295
25307
  isExported: true
25296
25308
  };
@@ -25298,7 +25310,7 @@ const LOWER = {
25298
25310
  description: _t("Converts a specified string to lowercase."),
25299
25311
  args: [arg("text (string)", _t("The string to convert to lowercase."))],
25300
25312
  compute: function(text) {
25301
- return toString(text).toLowerCase();
25313
+ return toLocaleString(text, this.locale).toLowerCase();
25302
25314
  },
25303
25315
  isExported: true
25304
25316
  };
@@ -25310,7 +25322,7 @@ const MID = {
25310
25322
  arg("extract_length (number)", _t("The length of the segment to extract."))
25311
25323
  ],
25312
25324
  compute: function(text, starting_at, extract_length) {
25313
- const _text = toString(text);
25325
+ const _text = toLocaleString(text, this.locale);
25314
25326
  const _starting_at = toNumber(starting_at, this.locale);
25315
25327
  const _extract_length = toNumber(extract_length, this.locale);
25316
25328
  if (_starting_at < 1) return new EvaluationError(_t("The starting_at argument (%s) must be positive greater than one.", _starting_at.toString()));
@@ -25323,7 +25335,7 @@ const PROPER = {
25323
25335
  description: _t("Capitalizes each word in a specified string."),
25324
25336
  args: [arg("text_to_capitalize (string)", _t("The text which will be returned with the first letter of each word in uppercase and all other letters in lowercase."))],
25325
25337
  compute: function(text) {
25326
- return toString(text).replace(wordRegex, (word) => {
25338
+ return toLocaleString(text, this.locale).replace(wordRegex, (word) => {
25327
25339
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
25328
25340
  });
25329
25341
  },
@@ -25471,9 +25483,9 @@ const REPLACE = {
25471
25483
  compute: function(text, position, length, newText) {
25472
25484
  const _position = toNumber(position, this.locale);
25473
25485
  if (_position < 1) return new EvaluationError(_t("The position (%s) must be greater than or equal to 1.", _position));
25474
- const _text = toString(text);
25486
+ const _text = toLocaleString(text, this.locale);
25475
25487
  const _length = toNumber(length, this.locale);
25476
- const _newText = toString(newText);
25488
+ const _newText = toLocaleString(newText, this.locale);
25477
25489
  return _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length);
25478
25490
  },
25479
25491
  isExported: true
@@ -25484,7 +25496,7 @@ const RIGHT = {
25484
25496
  compute: function(text, ...args) {
25485
25497
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
25486
25498
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters));
25487
- const _text = toString(text);
25499
+ const _text = toLocaleString(text, this.locale);
25488
25500
  const stringLength = _text.length;
25489
25501
  return _text.substring(stringLength - _numberOfCharacters, stringLength);
25490
25502
  },
@@ -25498,8 +25510,8 @@ const SEARCH = {
25498
25510
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
25499
25511
  ],
25500
25512
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
25501
- const _searchFor = toString(searchFor).toLowerCase();
25502
- const _textToSearch = toString(textToSearch).toLowerCase();
25513
+ const _searchFor = toLocaleString(searchFor, this.locale).toLowerCase();
25514
+ const _textToSearch = toLocaleString(textToSearch, this.locale).toLowerCase();
25503
25515
  const _startingAt = toNumber(startingAt, this.locale);
25504
25516
  if (_textToSearch === "") return {
25505
25517
  value: CellErrorType.GenericError,
@@ -25529,8 +25541,8 @@ const SPLIT = {
25529
25541
  arg(`remove_empty_text (boolean, default=${SPLIT_DEFAULT_REMOVE_EMPTY_TEXT})`, _t("Whether or not to remove empty text messages from the split results. The default behavior is to treat consecutive delimiters as one (if TRUE). If FALSE, empty cells values are added between consecutive delimiters."))
25530
25542
  ],
25531
25543
  compute: function(text, delimiter, splitByEach = { value: SPLIT_DEFAULT_SPLIT_BY_EACH }, removeEmptyText = { value: SPLIT_DEFAULT_REMOVE_EMPTY_TEXT }) {
25532
- const _text = toString(text);
25533
- const _delimiter = escapeRegExp(toString(delimiter));
25544
+ const _text = toLocaleString(text, this.locale);
25545
+ const _delimiter = escapeRegExp(toLocaleString(delimiter, this.locale));
25534
25546
  const _splitByEach = toBoolean(splitByEach);
25535
25547
  const _removeEmptyText = toBoolean(removeEmptyText);
25536
25548
  if (_delimiter.length <= 0) return new EvaluationError(_t("The delimiter (%s) must be not be empty.", _delimiter));
@@ -25552,10 +25564,10 @@ const SUBSTITUTE = {
25552
25564
  compute: function(textToSearch, searchFor, replaceWith, occurrenceNumber) {
25553
25565
  const _occurrenceNumber = toNumber(occurrenceNumber, this.locale);
25554
25566
  if (_occurrenceNumber < 0) return new EvaluationError(_t("The occurrenceNumber (%s) must be positive or null.", _occurrenceNumber));
25555
- const _textToSearch = toString(textToSearch);
25556
- const _searchFor = toString(searchFor);
25567
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
25568
+ const _searchFor = toLocaleString(searchFor, this.locale);
25557
25569
  if (_searchFor === "") return _textToSearch;
25558
- const _replaceWith = toString(replaceWith);
25570
+ const _replaceWith = toLocaleString(replaceWith, this.locale);
25559
25571
  const reg = new RegExp(escapeRegExp(_searchFor), "g");
25560
25572
  if (_occurrenceNumber === 0) return _textToSearch.replace(reg, _replaceWith);
25561
25573
  let n = 0;
@@ -25578,10 +25590,10 @@ const TEXTJOIN = {
25578
25590
  arg("texts (string, range<string>, repeating)", _t("Text item to join."))
25579
25591
  ],
25580
25592
  compute: function(delimiter, ignoreEmpty = { value: TEXTJOIN_DEFAULT_IGNORE_EMPTY }, ...textsOrArrays) {
25581
- const _delimiter = toString(delimiter);
25593
+ const _delimiter = toLocaleString(delimiter, this.locale);
25582
25594
  const _ignoreEmpty = toBoolean(ignoreEmpty);
25583
25595
  let n = 0;
25584
- return reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toString(a) === "") ? (n++ ? acc + _delimiter : "") + toString(a) : acc, "");
25596
+ return reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toLocaleString(a, this.locale) === "") ? (n++ ? acc + _delimiter : "") + toLocaleString(a, this.locale) : acc, "");
25585
25597
  },
25586
25598
  isExported: true
25587
25599
  };
@@ -25634,7 +25646,7 @@ const TRIM = {
25634
25646
  description: _t("Removes space characters."),
25635
25647
  args: [arg("text (string)", _t("The text or reference to a cell containing text to be trimmed."))],
25636
25648
  compute: function(text) {
25637
- return trimContent(toString(text));
25649
+ return trimContent(toLocaleString(text, this.locale));
25638
25650
  },
25639
25651
  isExported: true
25640
25652
  };
@@ -25642,7 +25654,7 @@ const UPPER = {
25642
25654
  description: _t("Converts a specified string to uppercase."),
25643
25655
  args: [arg("text (string)", _t("The string to convert to uppercase."))],
25644
25656
  compute: function(text) {
25645
- return toString(text).toUpperCase();
25657
+ return toLocaleString(text, this.locale).toUpperCase();
25646
25658
  },
25647
25659
  isExported: true
25648
25660
  };
@@ -25739,7 +25751,7 @@ const HYPERLINK = {
25739
25751
  args: [arg("url (string)", _t("The full URL of the link enclosed in quotation marks.")), arg("link_label (string, optional)", _t("The text to display in the cell, enclosed in quotation marks."))],
25740
25752
  compute: function(url, linkLabel) {
25741
25753
  const processedUrl = toString(url).trim();
25742
- const processedLabel = toString(linkLabel) || processedUrl;
25754
+ const processedLabel = toLocaleString(linkLabel, this.locale) || processedUrl;
25743
25755
  if (processedUrl === "") return processedLabel;
25744
25756
  return markdownLink(processedLabel, processedUrl);
25745
25757
  },
@@ -25841,6 +25853,9 @@ for (const category of categories) {
25841
25853
  });
25842
25854
  }
25843
25855
  }
25856
+ /** Formulas using one of these functions are never squished: they are always exported
25857
+ * as a full formula string, and they don't become the base formula of the next cells. */
25858
+ const NonSquishableFunctionRegistry = new Registry();
25844
25859
 
25845
25860
  //#endregion
25846
25861
  //#region src/types/xlsx.ts
@@ -36226,12 +36241,13 @@ var FilterMenu = class extends Component {
36226
36241
  }
36227
36242
  sortFilterZone(sortDirection) {
36228
36243
  const filterPosition = this.props.filterPosition;
36229
- const tableZone = this.table?.range.zone;
36244
+ const table = this.table;
36245
+ const tableZone = table?.range.zone;
36230
36246
  if (!filterPosition || !tableZone || tableZone.top === tableZone.bottom) return;
36231
36247
  const sheetId = this.env.model.getters.getActiveSheetId();
36232
36248
  const contentZone = {
36233
36249
  ...tableZone,
36234
- top: tableZone.top + 1
36250
+ top: tableZone.top + table.config.numberOfHeaders
36235
36251
  };
36236
36252
  const sortAnchor = {
36237
36253
  col: filterPosition.col,
@@ -55899,7 +55915,8 @@ var Squisher = class {
55899
55915
  * The result of this method is:
55900
55916
  * - if the cell is not a formula, returns the content as is and resets the base formula
55901
55917
  * - if the cell is a formula:
55902
- * - if there is no previous formula, or the normalized formula is different from the previous one, resets the base formula to this one and returns the full formula string
55918
+ * - if there is no previous formula, or the normalized formula is different from the previous one or it contains blacklisted functions (see NonSquishableFunctionRegistry),
55919
+ * resets the base formula to this one and returns the full formula string
55903
55920
  * - else, compares the literal values and range dependencies to the previous formula, and for each parameter:
55904
55921
  * - for numbers: returns a relative change (+N or -N) if possible, else the full number or "=" if unchanged
55905
55922
  * - for strings: returns the full string if changed, else "="
@@ -55910,6 +55927,10 @@ var Squisher = class {
55910
55927
  this.resetBase();
55911
55928
  return cell.content;
55912
55929
  }
55930
+ if (NonSquishableFunctionRegistry.getAll().some((functionName) => cell.compiledFormula.usesSymbol(functionName))) {
55931
+ this.resetBase();
55932
+ return cell.compiledFormula.toFormulaString(this.getters, this.rangeToStringOptions);
55933
+ }
55913
55934
  let numbers = [];
55914
55935
  let strings = [];
55915
55936
  let references = [];
@@ -81610,7 +81631,8 @@ const registries = {
81610
81631
  supportedPivotPositionalFormulaRegistry,
81611
81632
  pivotToFunctionValueRegistry,
81612
81633
  migrationStepRegistry,
81613
- chartJsExtensionRegistry
81634
+ chartJsExtensionRegistry,
81635
+ NonSquishableFunctionRegistry
81614
81636
  };
81615
81637
  const helpers = {
81616
81638
  arg,
@@ -81802,6 +81824,6 @@ const chartHelpers = {
81802
81824
  //#endregion
81803
81825
  export { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, CellErrorType, ClientDisconnectedError, CommandResult, CompiledFormula, CorePlugin, CoreViewPlugin, DEFAULT_LOCALE, DEFAULT_LOCALES, DispatchResult, EvaluationError, LocalTransportService, Model, PivotRuntimeDefinition, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, SpreadsheetPivotTable, UIPlugin, __info__, addFunction, addRenderingLayer, astToFormula, canExecuteInReadonly, categories, chartHelpers, components, constants, convertAstNodes, coreTypes, createAutocompleteArgumentsProvider, findCellInNewZone, functionCache, getCaretDownSvg, getCaretUpSvg, helpers, hooks, invalidateCFEvaluationCommands, invalidateChartEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, isCoreCommand, isSheetDependent, iterateAstNodes, links, load, lockedSheetAllowedCommands, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, stores, tokenColors, tokenize };
81804
81826
 
81805
- __info__.version = "19.2.24";
81806
- __info__.date = "2026-08-10T12:32:26.974Z";
81807
- __info__.hash = "31fa530";
81827
+ __info__.version = "19.2.26";
81828
+ __info__.date = "2026-08-21T15:30:14.701Z";
81829
+ __info__.hash = "b9d68c1";
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 19.2.24
6
- * @date 2026-08-10T12:32:26.974Z
7
- * @hash 31fa530
5
+ * @version 19.2.26
6
+ * @date 2026-08-21T15:30:14.701Z
7
+ * @hash b9d68c1
8
8
  */
9
9
 
10
10
  (function(exports, _odoo_owl) {
@@ -3753,6 +3753,18 @@ stores.inject(MyMetaStore, storeInstance);
3753
3753
  default: return "";
3754
3754
  }
3755
3755
  }
3756
+ /**
3757
+ * Only converts the decimal separator, ignore number format such as % or dates
3758
+ */
3759
+ function toLocaleString(data, locale) {
3760
+ const value = toValue(data);
3761
+ switch (typeof value) {
3762
+ case "string": return value;
3763
+ case "number": return value.toString().replace(".", locale.decimalSeparator);
3764
+ case "boolean": return value ? "TRUE" : "FALSE";
3765
+ default: return "";
3766
+ }
3767
+ }
3756
3768
  /** Normalize string by setting it to lowercase and replacing accent letters with plain letters */
3757
3769
  const normalizeString = memoize(function normalizeString(str) {
3758
3770
  return str.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
@@ -18293,7 +18305,7 @@ stores.inject(MyMetaStore, storeInstance);
18293
18305
  else if (_format === 0) {
18294
18306
  const rowSeparator = this.locale.decimalSeparator === "," ? "/" : ",";
18295
18307
  return transposeMatrix(_array).flatMap((row) => row.map((value) => {
18296
- return isEvaluationError(value.value) ? value.value : toString(value);
18308
+ return isEvaluationError(value.value) ? value.value : toLocaleString(value, this.locale);
18297
18309
  })).join(rowSeparator);
18298
18310
  } else return new EvaluationError(_t("Format must be 0 or 1"));
18299
18311
  },
@@ -24336,7 +24348,7 @@ stores.inject(MyMetaStore, storeInstance);
24336
24348
  description: _t("Concatenation of two values."),
24337
24349
  args: [arg("value1 (string)", _t("The value to which value2 will be appended.")), arg("value2 (string)", _t("The value to append to value1."))],
24338
24350
  compute: function(value1, value2) {
24339
- return toString(value1) + toString(value2);
24351
+ return toLocaleString(value1, this.locale) + toLocaleString(value2, this.locale);
24340
24352
  },
24341
24353
  isExported: true
24342
24354
  };
@@ -25205,7 +25217,7 @@ stores.inject(MyMetaStore, storeInstance);
25205
25217
  description: _t("Remove non-printable characters from a piece of text."),
25206
25218
  args: [arg("text (string)", _t("The text whose non-printable characters are to be removed."))],
25207
25219
  compute: function(text) {
25208
- const _text = toString(text);
25220
+ const _text = toLocaleString(text, this.locale);
25209
25221
  let cleanedStr = "";
25210
25222
  for (const char of _text) if (char && char.charCodeAt(0) > 31) cleanedStr += char;
25211
25223
  return cleanedStr;
@@ -25216,7 +25228,7 @@ stores.inject(MyMetaStore, storeInstance);
25216
25228
  description: _t("Appends strings to one another."),
25217
25229
  args: [arg("string (string, range<string>, repeating)", _t("String to append in sequence."))],
25218
25230
  compute: function(...datas) {
25219
- return reduceAny(datas, (acc, a) => acc + toString(a), "");
25231
+ return reduceAny(datas, (acc, a) => acc + toLocaleString(a, this.locale), "");
25220
25232
  },
25221
25233
  isExported: true
25222
25234
  };
@@ -25224,7 +25236,7 @@ stores.inject(MyMetaStore, storeInstance);
25224
25236
  description: _t("Tests whether two strings are identical."),
25225
25237
  args: [arg("string1 (string)", _t("The first string to compare.")), arg("string2 (string)", _t("The second string to compare."))],
25226
25238
  compute: function(string1, string2) {
25227
- return toString(string1) === toString(string2);
25239
+ return toLocaleString(string1, this.locale) === toLocaleString(string2, this.locale);
25228
25240
  },
25229
25241
  isExported: true
25230
25242
  };
@@ -25236,8 +25248,8 @@ stores.inject(MyMetaStore, storeInstance);
25236
25248
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
25237
25249
  ],
25238
25250
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
25239
- const _searchFor = toString(searchFor);
25240
- const _textToSearch = toString(textToSearch);
25251
+ const _searchFor = toLocaleString(searchFor, this.locale);
25252
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
25241
25253
  const _startingAt = toNumber(startingAt, this.locale);
25242
25254
  if (_textToSearch === "") return new EvaluationError(_t("The text_to_search must be non-empty."));
25243
25255
  if (_startingAt < 1) return new EvaluationError(_t("The starting_at (%s) must be greater than or equal to 1.", _startingAt));
@@ -25274,8 +25286,8 @@ stores.inject(MyMetaStore, storeInstance);
25274
25286
  description: _t("Concatenates elements of arrays with delimiter."),
25275
25287
  args: [arg("delimiter (string)", _t("The character or string to place between each concatenated value.")), arg("value_or_array (string, range<string>, repeating)", _t("Value to be appended using delimiter."))],
25276
25288
  compute: function(delimiter, ...valuesOrArrays) {
25277
- const _delimiter = toString(delimiter);
25278
- return reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toString(a), "");
25289
+ const _delimiter = toLocaleString(delimiter, this.locale);
25290
+ return reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toLocaleString(a, this.locale), "");
25279
25291
  }
25280
25292
  };
25281
25293
  const LEFT = {
@@ -25284,7 +25296,7 @@ stores.inject(MyMetaStore, storeInstance);
25284
25296
  compute: function(text, ...args) {
25285
25297
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
25286
25298
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters));
25287
- return toString(text).substring(0, _numberOfCharacters);
25299
+ return toLocaleString(text, this.locale).substring(0, _numberOfCharacters);
25288
25300
  },
25289
25301
  isExported: true
25290
25302
  };
@@ -25292,7 +25304,7 @@ stores.inject(MyMetaStore, storeInstance);
25292
25304
  description: _t("Length of a string."),
25293
25305
  args: [arg("text (string)", _t("The string whose length will be returned."))],
25294
25306
  compute: function(text) {
25295
- return toString(text).length;
25307
+ return toLocaleString(text, this.locale).length;
25296
25308
  },
25297
25309
  isExported: true
25298
25310
  };
@@ -25300,7 +25312,7 @@ stores.inject(MyMetaStore, storeInstance);
25300
25312
  description: _t("Converts a specified string to lowercase."),
25301
25313
  args: [arg("text (string)", _t("The string to convert to lowercase."))],
25302
25314
  compute: function(text) {
25303
- return toString(text).toLowerCase();
25315
+ return toLocaleString(text, this.locale).toLowerCase();
25304
25316
  },
25305
25317
  isExported: true
25306
25318
  };
@@ -25312,7 +25324,7 @@ stores.inject(MyMetaStore, storeInstance);
25312
25324
  arg("extract_length (number)", _t("The length of the segment to extract."))
25313
25325
  ],
25314
25326
  compute: function(text, starting_at, extract_length) {
25315
- const _text = toString(text);
25327
+ const _text = toLocaleString(text, this.locale);
25316
25328
  const _starting_at = toNumber(starting_at, this.locale);
25317
25329
  const _extract_length = toNumber(extract_length, this.locale);
25318
25330
  if (_starting_at < 1) return new EvaluationError(_t("The starting_at argument (%s) must be positive greater than one.", _starting_at.toString()));
@@ -25325,7 +25337,7 @@ stores.inject(MyMetaStore, storeInstance);
25325
25337
  description: _t("Capitalizes each word in a specified string."),
25326
25338
  args: [arg("text_to_capitalize (string)", _t("The text which will be returned with the first letter of each word in uppercase and all other letters in lowercase."))],
25327
25339
  compute: function(text) {
25328
- return toString(text).replace(wordRegex, (word) => {
25340
+ return toLocaleString(text, this.locale).replace(wordRegex, (word) => {
25329
25341
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
25330
25342
  });
25331
25343
  },
@@ -25473,9 +25485,9 @@ stores.inject(MyMetaStore, storeInstance);
25473
25485
  compute: function(text, position, length, newText) {
25474
25486
  const _position = toNumber(position, this.locale);
25475
25487
  if (_position < 1) return new EvaluationError(_t("The position (%s) must be greater than or equal to 1.", _position));
25476
- const _text = toString(text);
25488
+ const _text = toLocaleString(text, this.locale);
25477
25489
  const _length = toNumber(length, this.locale);
25478
- const _newText = toString(newText);
25490
+ const _newText = toLocaleString(newText, this.locale);
25479
25491
  return _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length);
25480
25492
  },
25481
25493
  isExported: true
@@ -25486,7 +25498,7 @@ stores.inject(MyMetaStore, storeInstance);
25486
25498
  compute: function(text, ...args) {
25487
25499
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
25488
25500
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters));
25489
- const _text = toString(text);
25501
+ const _text = toLocaleString(text, this.locale);
25490
25502
  const stringLength = _text.length;
25491
25503
  return _text.substring(stringLength - _numberOfCharacters, stringLength);
25492
25504
  },
@@ -25500,8 +25512,8 @@ stores.inject(MyMetaStore, storeInstance);
25500
25512
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
25501
25513
  ],
25502
25514
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
25503
- const _searchFor = toString(searchFor).toLowerCase();
25504
- const _textToSearch = toString(textToSearch).toLowerCase();
25515
+ const _searchFor = toLocaleString(searchFor, this.locale).toLowerCase();
25516
+ const _textToSearch = toLocaleString(textToSearch, this.locale).toLowerCase();
25505
25517
  const _startingAt = toNumber(startingAt, this.locale);
25506
25518
  if (_textToSearch === "") return {
25507
25519
  value: CellErrorType.GenericError,
@@ -25531,8 +25543,8 @@ stores.inject(MyMetaStore, storeInstance);
25531
25543
  arg(`remove_empty_text (boolean, default=${SPLIT_DEFAULT_REMOVE_EMPTY_TEXT})`, _t("Whether or not to remove empty text messages from the split results. The default behavior is to treat consecutive delimiters as one (if TRUE). If FALSE, empty cells values are added between consecutive delimiters."))
25532
25544
  ],
25533
25545
  compute: function(text, delimiter, splitByEach = { value: SPLIT_DEFAULT_SPLIT_BY_EACH }, removeEmptyText = { value: SPLIT_DEFAULT_REMOVE_EMPTY_TEXT }) {
25534
- const _text = toString(text);
25535
- const _delimiter = escapeRegExp(toString(delimiter));
25546
+ const _text = toLocaleString(text, this.locale);
25547
+ const _delimiter = escapeRegExp(toLocaleString(delimiter, this.locale));
25536
25548
  const _splitByEach = toBoolean(splitByEach);
25537
25549
  const _removeEmptyText = toBoolean(removeEmptyText);
25538
25550
  if (_delimiter.length <= 0) return new EvaluationError(_t("The delimiter (%s) must be not be empty.", _delimiter));
@@ -25554,10 +25566,10 @@ stores.inject(MyMetaStore, storeInstance);
25554
25566
  compute: function(textToSearch, searchFor, replaceWith, occurrenceNumber) {
25555
25567
  const _occurrenceNumber = toNumber(occurrenceNumber, this.locale);
25556
25568
  if (_occurrenceNumber < 0) return new EvaluationError(_t("The occurrenceNumber (%s) must be positive or null.", _occurrenceNumber));
25557
- const _textToSearch = toString(textToSearch);
25558
- const _searchFor = toString(searchFor);
25569
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
25570
+ const _searchFor = toLocaleString(searchFor, this.locale);
25559
25571
  if (_searchFor === "") return _textToSearch;
25560
- const _replaceWith = toString(replaceWith);
25572
+ const _replaceWith = toLocaleString(replaceWith, this.locale);
25561
25573
  const reg = new RegExp(escapeRegExp(_searchFor), "g");
25562
25574
  if (_occurrenceNumber === 0) return _textToSearch.replace(reg, _replaceWith);
25563
25575
  let n = 0;
@@ -25580,10 +25592,10 @@ stores.inject(MyMetaStore, storeInstance);
25580
25592
  arg("texts (string, range<string>, repeating)", _t("Text item to join."))
25581
25593
  ],
25582
25594
  compute: function(delimiter, ignoreEmpty = { value: TEXTJOIN_DEFAULT_IGNORE_EMPTY }, ...textsOrArrays) {
25583
- const _delimiter = toString(delimiter);
25595
+ const _delimiter = toLocaleString(delimiter, this.locale);
25584
25596
  const _ignoreEmpty = toBoolean(ignoreEmpty);
25585
25597
  let n = 0;
25586
- return reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toString(a) === "") ? (n++ ? acc + _delimiter : "") + toString(a) : acc, "");
25598
+ return reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toLocaleString(a, this.locale) === "") ? (n++ ? acc + _delimiter : "") + toLocaleString(a, this.locale) : acc, "");
25587
25599
  },
25588
25600
  isExported: true
25589
25601
  };
@@ -25636,7 +25648,7 @@ stores.inject(MyMetaStore, storeInstance);
25636
25648
  description: _t("Removes space characters."),
25637
25649
  args: [arg("text (string)", _t("The text or reference to a cell containing text to be trimmed."))],
25638
25650
  compute: function(text) {
25639
- return trimContent(toString(text));
25651
+ return trimContent(toLocaleString(text, this.locale));
25640
25652
  },
25641
25653
  isExported: true
25642
25654
  };
@@ -25644,7 +25656,7 @@ stores.inject(MyMetaStore, storeInstance);
25644
25656
  description: _t("Converts a specified string to uppercase."),
25645
25657
  args: [arg("text (string)", _t("The string to convert to uppercase."))],
25646
25658
  compute: function(text) {
25647
- return toString(text).toUpperCase();
25659
+ return toLocaleString(text, this.locale).toUpperCase();
25648
25660
  },
25649
25661
  isExported: true
25650
25662
  };
@@ -25741,7 +25753,7 @@ stores.inject(MyMetaStore, storeInstance);
25741
25753
  args: [arg("url (string)", _t("The full URL of the link enclosed in quotation marks.")), arg("link_label (string, optional)", _t("The text to display in the cell, enclosed in quotation marks."))],
25742
25754
  compute: function(url, linkLabel) {
25743
25755
  const processedUrl = toString(url).trim();
25744
- const processedLabel = toString(linkLabel) || processedUrl;
25756
+ const processedLabel = toLocaleString(linkLabel, this.locale) || processedUrl;
25745
25757
  if (processedUrl === "") return processedLabel;
25746
25758
  return markdownLink(processedLabel, processedUrl);
25747
25759
  },
@@ -25843,6 +25855,9 @@ stores.inject(MyMetaStore, storeInstance);
25843
25855
  });
25844
25856
  }
25845
25857
  }
25858
+ /** Formulas using one of these functions are never squished: they are always exported
25859
+ * as a full formula string, and they don't become the base formula of the next cells. */
25860
+ const NonSquishableFunctionRegistry = new Registry();
25846
25861
 
25847
25862
  //#endregion
25848
25863
  //#region src/types/xlsx.ts
@@ -36228,12 +36243,13 @@ stores.inject(MyMetaStore, storeInstance);
36228
36243
  }
36229
36244
  sortFilterZone(sortDirection) {
36230
36245
  const filterPosition = this.props.filterPosition;
36231
- const tableZone = this.table?.range.zone;
36246
+ const table = this.table;
36247
+ const tableZone = table?.range.zone;
36232
36248
  if (!filterPosition || !tableZone || tableZone.top === tableZone.bottom) return;
36233
36249
  const sheetId = this.env.model.getters.getActiveSheetId();
36234
36250
  const contentZone = {
36235
36251
  ...tableZone,
36236
- top: tableZone.top + 1
36252
+ top: tableZone.top + table.config.numberOfHeaders
36237
36253
  };
36238
36254
  const sortAnchor = {
36239
36255
  col: filterPosition.col,
@@ -55901,7 +55917,8 @@ stores.inject(MyMetaStore, storeInstance);
55901
55917
  * The result of this method is:
55902
55918
  * - if the cell is not a formula, returns the content as is and resets the base formula
55903
55919
  * - if the cell is a formula:
55904
- * - if there is no previous formula, or the normalized formula is different from the previous one, resets the base formula to this one and returns the full formula string
55920
+ * - if there is no previous formula, or the normalized formula is different from the previous one or it contains blacklisted functions (see NonSquishableFunctionRegistry),
55921
+ * resets the base formula to this one and returns the full formula string
55905
55922
  * - else, compares the literal values and range dependencies to the previous formula, and for each parameter:
55906
55923
  * - for numbers: returns a relative change (+N or -N) if possible, else the full number or "=" if unchanged
55907
55924
  * - for strings: returns the full string if changed, else "="
@@ -55912,6 +55929,10 @@ stores.inject(MyMetaStore, storeInstance);
55912
55929
  this.resetBase();
55913
55930
  return cell.content;
55914
55931
  }
55932
+ if (NonSquishableFunctionRegistry.getAll().some((functionName) => cell.compiledFormula.usesSymbol(functionName))) {
55933
+ this.resetBase();
55934
+ return cell.compiledFormula.toFormulaString(this.getters, this.rangeToStringOptions);
55935
+ }
55915
55936
  let numbers = [];
55916
55937
  let strings = [];
55917
55938
  let references = [];
@@ -81612,7 +81633,8 @@ stores.inject(MyMetaStore, storeInstance);
81612
81633
  supportedPivotPositionalFormulaRegistry,
81613
81634
  pivotToFunctionValueRegistry,
81614
81635
  migrationStepRegistry,
81615
- chartJsExtensionRegistry
81636
+ chartJsExtensionRegistry,
81637
+ NonSquishableFunctionRegistry
81616
81638
  };
81617
81639
  const helpers = {
81618
81640
  arg,
@@ -81862,8 +81884,8 @@ exports.stores = stores;
81862
81884
  exports.tokenColors = tokenColors;
81863
81885
  exports.tokenize = tokenize;
81864
81886
 
81865
- __info__.version = "19.2.24";
81866
- __info__.date = "2026-08-10T12:32:26.974Z";
81867
- __info__.hash = "31fa530";
81887
+ __info__.version = "19.2.26";
81888
+ __info__.date = "2026-08-21T15:30:14.701Z";
81889
+ __info__.hash = "b9d68c1";
81868
81890
 
81869
81891
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);