@odoo/o-spreadsheet
Version:
A spreadsheet component
1,560 lines (1,549 loc) • 2.95 MB
JavaScript
/**
* This file is generated by o-spreadsheet build tools. Do not edit it.
* @see https://github.com/odoo/o-spreadsheet
* @version 19.3.4
* @date 2026-05-15T07:07:34.417Z
* @hash 1dc7b42
*/
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
//#region \0rolldown/runtime.js
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
var __exportAll = (all, no_symbols) => {
let target = {};
for (var name in all) {
__defProp(target, name, {
get: all[name],
enumerable: true
});
}
if (!no_symbols) {
__defProp(target, Symbol.toStringTag, { value: "Module" });
}
return target;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) {
__defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
}
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
let _odoo_owl = require("@odoo/owl");
//#region src/dom_mock.ts
if (typeof globalThis.OffscreenCanvas === "undefined") {
class MockOffscreenCanvasRenderingContext2D {
constructor() {
return proxy(this);
}
save() {}
restore() {}
measureText(text) {
return { width: text.length };
}
}
class MockOffscreenCanvas {
constructor(width, height) {
return proxy(this);
}
getContext(contextId) {
if (contextId === "2d") return new MockOffscreenCanvasRenderingContext2D();
return null;
}
}
function proxy(target) {
return new Proxy(target, { get: function(obj, prop, receiver) {
if (Reflect.has(obj, prop)) return Reflect.get(obj, prop, receiver);
throw new Error(`OffscreenCanvas mock: "${String(prop)}" is not implemented.\nAdd it to MockOffscreenCanvas or MockOffscreenCanvasRenderingContext2D if needed.`);
} });
}
globalThis.OffscreenCanvas = MockOffscreenCanvas;
}
if (typeof globalThis.DOMParser === "undefined") globalThis.DOMParser = class DOMParser {
parseFromString() {
return {
querySelector() {
return null;
},
querySelectorAll() {
return [];
},
body: {}
};
}
};
if (typeof globalThis.document === "undefined") {
const noop = function() {};
globalThis.document = {
implementation: { createDocument() {
return {};
} },
addEventListener: noop,
removeEventListener: noop,
querySelectorAll() {
return [];
},
querySelector() {
return null;
},
createElement() {
return {
setAttribute() {},
getBoundingClientRect() {
return {
width: 0,
height: 0
};
}
};
},
createTextNode() {
return {};
},
cookie: "",
head: { querySelectorAll() {
return [];
} },
body: {
classList: {
add: noop,
remove: noop,
contains: noop
},
contains() {
return false;
},
appendChild() {},
removeChild() {}
}
};
}
//#endregion
//#region src/actions/action.ts
function createActions(menuItems) {
return menuItems.map(createAction).sort((a, b) => a.sequence - b.sequence);
}
let nextItemId = 1;
function createAction(item) {
const name = item.name;
const children = item.children;
const description = item.description;
const shortcut = item.shortcut;
const icon = item.icon;
const secondaryIcon = item.secondaryIcon;
const itemId = item.id || nextItemId++;
const isEnabled = item.isEnabled ? item.isEnabled : () => true;
return {
id: itemId.toString(),
name: typeof name === "function" ? name : () => name,
isVisible: item.isVisible ? item.isVisible : () => true,
isEnabled,
isActive: item.isActive,
execute: item.execute ? (env, isMiddleClick) => {
if (isEnabled(env)) return item.execute(env, isMiddleClick);
} : void 0,
children: children ? (env) => {
return children.map((child) => typeof child === "function" ? child(env) : child).flat().map(createAction).sort((a, b) => a.sequence - b.sequence);
} : () => [],
isReadonlyAllowed: item.isReadonlyAllowed || false,
isEnabledOnLockedSheet: item.isEnabledOnLockedSheet || false,
separator: item.separator || false,
icon: typeof icon === "function" ? icon : () => icon || "",
iconColor: item.iconColor,
secondaryIcon: typeof secondaryIcon === "function" ? secondaryIcon : () => secondaryIcon || "",
description: typeof description === "function" ? description : () => description || "",
shortcut: shortcut || "",
textColor: item.textColor,
sequence: item.sequence || 0,
onStartHover: item.onStartHover,
onStopHover: item.onStopHover
};
}
function getMenuItemsAndSeparators(env, actions) {
const menuItemsAndSeparators = [];
for (let i = 0; i < actions.length; i++) {
const menuItem = actions[i];
if (menuItem.isVisible(env) && (!isRootMenu(menuItem) || hasVisibleChildren(env, menuItem))) menuItemsAndSeparators.push(menuItem);
if (menuItem.separator && i !== actions.length - 1 && menuItemsAndSeparators[menuItemsAndSeparators.length - 1] !== "separator") menuItemsAndSeparators.push("separator");
}
if (menuItemsAndSeparators[menuItemsAndSeparators.length - 1] === "separator") menuItemsAndSeparators.pop();
if (menuItemsAndSeparators.length === 1 && menuItemsAndSeparators[0] === "separator") return [];
return menuItemsAndSeparators;
}
function isRootMenu(menu) {
return !menu.execute;
}
function hasVisibleChildren(env, menu) {
return menu.children(env).some((child) => child.isVisible(env));
}
function isMenuItemEnabled(env, menu) {
const children = menu.children?.(env);
if (children.length) return children.some((child) => isMenuItemEnabled(env, child));
else {
if (menu.isEnabled(env)) return env.model.getters.isReadonly() ? menu.isReadonlyAllowed : true;
return false;
}
}
//#endregion
//#region src/constants.ts
const CANVAS_SHIFT = .5;
const HIGHLIGHT_COLOR = "#017E84";
const BACKGROUND_HEADER_COLOR = "#F8F9FA";
const BACKGROUND_HEADER_SELECTED_COLOR = "#E8EAED";
const BACKGROUND_HEADER_ACTIVE_COLOR = "#595959";
const TEXT_HEADER_COLOR = "#666666";
const SELECTION_BORDER_COLOR = "#3266ca";
const HEADER_BORDER_COLOR = "#C0C0C0";
const CELL_BORDER_COLOR = "#E2E3E3";
const BACKGROUND_CHART_COLOR = "#FFFFFF";
const DEFAULT_COLOR_SCALE_MIDPOINT_COLOR = 11982760;
const LINK_COLOR = HIGHLIGHT_COLOR;
const FILTERS_COLOR = "#188038";
const FROZEN_PANE_HEADER_BORDER_COLOR = "#BCBCBC";
const FROZEN_PANE_BORDER_COLOR = "#DADFE8";
const COMPOSER_ASSISTANT_COLOR = "light-dark(#9B359B, #B972A6)";
const COLOR_TRANSPARENT = "#00000000";
const TABLE_HOVER_BACKGROUND_COLOR = "#017E8414";
const CHART_WATERFALL_POSITIVE_COLOR = "#4EA7F2";
const CHART_WATERFALL_NEGATIVE_COLOR = "#EA6175";
const CHART_WATERFALL_SUBTOTAL_COLOR = "#AAAAAA";
const GRAY_900 = "#111827";
const GRAY_400 = "#ced4da";
const GRAY_300 = "#D8DADD";
const GRAY_200 = "#E7E9ED";
const TEXT_BODY = "#374151";
const TEXT_BODY_MUTED = TEXT_BODY + "C2";
const ACTION_COLOR = HIGHLIGHT_COLOR;
const CHART_TITLE_FONT_SIZE = 16;
const DEFAULT_CHART_COLOR_SCALE = {
minColor: "#FFF5EB",
midColor: "#FD8D3C",
maxColor: "#7F2704"
};
const PIVOT_TOKEN_COLOR = "#F28C28";
const COLOR_PICKER_DEFAULTS = [
"#000000",
"#434343",
"#666666",
"#999999",
"#B7B7B7",
"#CCCCCC",
"#D9D9D9",
"#EFEFEF",
"#F3F3F3",
"#FFFFFF",
"#980000",
"#FF0000",
"#FF9900",
"#FFFF00",
"#00FF00",
"#00FFFF",
"#4A86E8",
"#0000FF",
"#9900FF",
"#FF00FF",
"#E6B8AF",
"#F4CCCC",
"#FCE5CD",
"#FFF2CC",
"#D9EAD3",
"#D0E0E3",
"#C9DAF8",
"#CFE2F3",
"#D9D2E9",
"#EAD1DC",
"#DD7E6B",
"#EA9999",
"#F9CB9C",
"#FFE599",
"#B6D7A8",
"#A2C4C9",
"#A4C2F4",
"#9FC5E8",
"#B4A7D6",
"#D5A6BD",
"#CC4125",
"#E06666",
"#F6B26B",
"#FFD966",
"#93C47D",
"#76A5AF",
"#6D9EEB",
"#6FA8DC",
"#8E7CC3",
"#C27BA0",
"#A61C00",
"#CC0000",
"#E69138",
"#F1C232",
"#6AA84F",
"#45818E",
"#3C78D8",
"#3D85C6",
"#674EA7",
"#A64D79",
"#85200C",
"#990000",
"#B45F06",
"#BF9000",
"#38761D",
"#134F5C",
"#1155CC",
"#0B5394",
"#351C75",
"#741B47",
"#5B0F00",
"#660000",
"#783F04",
"#7F6000",
"#274E13",
"#0C343D",
"#1C4587",
"#073763",
"#20124D",
"#4C1130"
];
const DEFAULT_CELL_HEIGHT = 23;
const FOOTER_HEIGHT = 2 * 23;
const MENU_SEPARATOR_BORDER_WIDTH = 1;
const MENU_SEPARATOR_PADDING = 5;
const MENU_SEPARATOR_HEIGHT = 1 + 2 * 5;
const ZOOM_VALUES = [
50,
75,
100,
125,
150,
200
];
const DEFAULT_STYLE = {
align: "left",
verticalAlign: "bottom",
wrapping: "overflow",
bold: false,
italic: false,
strikethrough: false,
underline: false,
fontSize: 10,
fillColor: "",
textColor: "",
rotation: 0,
hideGridLines: false
};
const ROTATION_EPSILON = .001;
const DEFAULT_NUMBER_STYLE = {
...DEFAULT_STYLE,
align: "right"
};
const DEFAULT_VERTICAL_ALIGN = DEFAULT_STYLE.verticalAlign;
const DEFAULT_WRAPPING_MODE = DEFAULT_STYLE.wrapping;
const DEFAULT_FONT_SIZE = DEFAULT_STYLE.fontSize;
const DEFAULT_FONT = "'Roboto', arial";
const DEFAULT_BORDER_DESC = {
style: "thin",
color: "#000000"
};
const DEFAULT_REVISION_ID = "START_REVISION";
const DEFAULT_GAUGE_LOWER_COLOR = "#EA6175";
const DEFAULT_GAUGE_MIDDLE_COLOR = "#FFD86D";
const DEFAULT_GAUGE_UPPER_COLOR = "#43C5B1";
const DEFAULT_SCORECARD_BASELINE_MODE = "difference";
const DEFAULT_SCORECARD_BASELINE_COLOR_UP = "#43C5B1";
const DEFAULT_SCORECARD_BASELINE_COLOR_DOWN = "#EA6175";
const LINE_FILL_TRANSPARENCY = .4;
const FORBIDDEN_SHEETNAME_CHARS = [
"'",
"*",
"?",
"/",
"\\",
"[",
"]"
];
const FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX = /'|\*|\?|\/|\\|\[|\]/;
let DEFAULT_SHEETVIEW_SIZE = 0;
/**
* The viewport dimensions are usually set by one of the components
* (i.e. when grid component is mounted) to properly reflect its state in the DOM.
* In the absence of a component (standalone model), is it mandatory to set reasonable default values
* to ensure the correct operation.
*/
function getDefaultSheetViewSize() {
return DEFAULT_SHEETVIEW_SIZE;
}
function setDefaultSheetViewSize(size) {
DEFAULT_SHEETVIEW_SIZE = size;
}
const MAXIMAL_FREEZABLE_RATIO = .85;
const FONT_SIZES = [
6,
7,
8,
9,
10,
11,
12,
14,
18,
24,
36
];
const PIVOT_STATIC_TABLE_CONFIG = {
hasFilters: false,
totalRow: false,
firstColumn: true,
lastColumn: false,
numberOfHeaders: 1,
bandedRows: true,
bandedColumns: false,
styleId: "TableStyleMedium5",
automaticAutofill: false
};
const PIVOT_INSERT_TABLE_STYLE_ID = "PivotTableStyleMedium12";
const PIVOT_MAX_NUMBER_OF_CELLS = 5e5;
const DEFAULT_CURRENCY = {
symbol: "$",
position: "before",
decimalPlaces: 2,
code: "",
name: "Dollar"
};
const DEFAULT_CAROUSEL_TITLE_STYLE = {
fontSize: 16,
color: TEXT_BODY
};
const DEFAULT_TOKEN_COLOR = "light-dark(#000000, #ffffff)";
const functionColor = DEFAULT_TOKEN_COLOR;
const operatorColor = "#3da4ab";
const tokenColors = {
OPERATOR: operatorColor,
NUMBER: "#02c39a",
STRING: "#00a82d",
FUNCTION: functionColor,
DEBUGGER: operatorColor,
LEFT_PAREN: functionColor,
RIGHT_PAREN: functionColor,
ARG_SEPARATOR: functionColor,
ORPHAN_RIGHT_PAREN: "#ff0000"
};
//#endregion
//#region src/types/misc.ts
const borderStyles = [
"thin",
"medium",
"thick",
"dashed",
"dotted"
];
function isMatrix(x) {
return Array.isArray(x) && Array.isArray(x[0]);
}
//#endregion
//#region src/helpers/misc.ts
const sanitizeSheetNameRegex = new RegExp(FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX, "g");
function isCloneable(obj) {
return "clone" in obj && obj.clone instanceof Function;
}
/**
* Escapes a string to use as a literal string in a RegExp.
* @url https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
*/
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
* Deep copy arrays, plain objects and primitive values.
* Throws an error for other types such as class instances.
* Sparse arrays remain sparse.
*/
function deepCopy(obj) {
switch (typeof obj) {
case "object": {
if (obj === null) return obj;
else if (isCloneable(obj)) return obj.clone();
else if (!(isPlainObject(obj) || obj instanceof Array)) throw new Error("Unsupported type: only objects and arrays are supported");
const result = Array.isArray(obj) ? new Array(obj.length) : {};
if (Array.isArray(obj)) {
for (let i = 0, len = obj.length; i < len; i++) if (i in obj) result[i] = deepCopy(obj[i]);
} else for (const key in obj) result[key] = deepCopy(obj[key]);
return result;
}
case "number":
case "string":
case "boolean":
case "function":
case "undefined": return obj;
default: throw new Error(`Unsupported type: ${typeof obj}`);
}
}
/**
* Check if the object is a plain old javascript object.
*/
function isPlainObject(obj) {
return typeof obj === "object" && obj !== null && (obj?.constructor === Object || obj?.constructor === void 0);
}
/**
* Sanitize the name of a sheet, by eventually removing quotes.
*/
function getUnquotedSheetName(sheetName) {
return unquote(sheetName, "'");
}
/**
* Remove quotes from a quoted string.
*/
function unquote(string, quoteChar = "\"") {
if (string.startsWith(quoteChar)) string = string.slice(1);
if (string.endsWith(quoteChar)) string = string.slice(0, -1);
return string;
}
/**
* Add quotes around the sheet name or any symbol name if it contains at least one non alphanumeric character.
*/
function getCanonicalSymbolName(symbolName) {
if (symbolName.match(/\w/g)?.length !== symbolName.length) symbolName = `'${symbolName}'`;
return symbolName;
}
/** Replace the excel-excluded characters of a sheetName */
function sanitizeSheetName(sheetName, replacementChar = " ") {
return sheetName.replace(sanitizeSheetNameRegex, replacementChar);
}
function clip(val, min, max) {
return val < min ? min : val > max ? max : val;
}
/**
* Create a range from start (included) to end (excluded).
* range(10, 13) => [10, 11, 12]
* range(2, 8, 2) => [2, 4, 6]
*/
function range(start, end, step = 1) {
if (end <= start && step > 0) return [];
if (step === 0) throw new Error("range() step must not be zero");
const length = Math.ceil(Math.abs((end - start) / step));
const array = Array(length);
for (let i = 0; i < length; i++) array[i] = start + i * step;
return array;
}
/**
* Groups consecutive numbers.
* The input array is assumed to be sorted
* @param numbers
*/
function groupConsecutive(numbers) {
return numbers.reduce((groups, currentRow, index, rows) => {
if (Math.abs(currentRow - rows[index - 1]) === 1) groups[groups.length - 1].push(currentRow);
else groups.push([currentRow]);
return groups;
}, []);
}
/**
* Create one generator from two generators by linking
* each item of the first generator to the next item of
* the second generator.
*
* Let's say generator G1 yields A, B, C and generator G2 yields X, Y, Z.
* The resulting generator of `linkNext(G1, G2)` will yield A', B', C'
* where `A' = A & {next: Y}`, `B' = B & {next: Z}` and `C' = C & {next: undefined}`
* @param generator
* @param nextGenerator
*/
function* linkNext(generator, nextGenerator) {
nextGenerator.next();
for (const item of generator) {
const nextItem = nextGenerator.next();
yield {
...item,
next: nextItem.done ? void 0 : nextItem.value
};
}
}
function isBoolean(str) {
const upperCased = str.toUpperCase();
return upperCased === "TRUE" || upperCased === "FALSE";
}
const MARKDOWN_LINK_REGEX = /^\[(.+)\]\((.+)\)$/;
const WEB_LINK_REGEX = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/;
function isMarkdownLink(str) {
return MARKDOWN_LINK_REGEX.test(str);
}
/**
* Check if the string is a web link.
* e.g. http://odoo.com
*/
function isWebLink(str) {
return WEB_LINK_REGEX.test(str);
}
/**
* Build a markdown link from a label and an url
*/
function markdownLink(label, url) {
return `[${label}](${url})`;
}
function parseMarkdownLink(str) {
const matches = str.match(MARKDOWN_LINK_REGEX) || [];
const label = matches[1];
const url = matches[2];
if (!label || !url) throw new Error(`Could not parse markdown link ${str}.`);
return {
label,
url
};
}
const O_SPREADSHEET_LINK_PREFIX = "o-spreadsheet://";
function isSheetUrl(url) {
return url.startsWith(O_SPREADSHEET_LINK_PREFIX);
}
function buildSheetLink(sheetId) {
return `${O_SPREADSHEET_LINK_PREFIX}${sheetId}`;
}
/**
* Parse a sheet link and return the sheet id
*/
function parseSheetUrl(sheetLink) {
if (sheetLink.startsWith(O_SPREADSHEET_LINK_PREFIX)) return sheetLink.slice(16);
throw new Error(`${sheetLink} is not a valid sheet link`);
}
/**
* This helper function can be used as a type guard when filtering arrays.
* const foo: number[] = [1, 2, undefined, 4].filter(isDefined)
*/
function isDefined(argument) {
return argument !== void 0;
}
/**
* Check if all the values of an object, and all the values of the objects inside of it, are undefined.
*/
function isObjectEmptyRecursive(argument) {
if (argument === void 0) return true;
return Object.values(argument).every((value) => typeof value === "object" ? isObjectEmptyRecursive(value) : !value);
}
/**
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds. If `immediate` is passed, the function is called is called
* immediately on the first call and the debouncing is triggered starting the second
* call in the defined time window.
*
* Example:
* debouncedFunction = debounce(() => console.log('Hello!'), 250);
* debouncedFunction(); debouncedFunction(); // Will log 'Hello!' after 250ms
*
* debouncedFunction = debounce(() => console.log('Hello!'), 250, true);
* debouncedFunction(); debouncedFunction(); // Will log 'Hello!' and relog it after 250ms
*
*
* Also decorate the argument function with two methods: stopDebounce and isDebouncePending.
*
* Inspired by https://davidwalsh.name/javascript-debounce-function
*/
function debounce(func, wait, immediate) {
let timeout = void 0;
let firstCalled = false;
const debounced = function() {
const context = this;
const args = Array.from(arguments);
if (!firstCalled && immediate) {
firstCalled = true;
return func.apply(context, args);
}
function later() {
timeout = void 0;
firstCalled = false;
func.apply(context, args);
}
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
debounced.isDebouncePending = () => timeout !== void 0;
debounced.stopDebounce = () => {
clearTimeout(timeout);
};
return debounced;
}
/**
* Creates a batched version of a callback so that all calls to it in the same
* microtick will only call the original callback once.
*
* @param callback the callback to batch
* @returns a batched version of the original callback
*
* Copied from odoo/owl repo.
*/
function batched(callback) {
let scheduled = false;
return async (...args) => {
if (!scheduled) {
scheduled = true;
await Promise.resolve();
scheduled = false;
callback(...args);
}
};
}
/** Returns a copy of the function `callback` that can only be called
* at most once every `delay` milliseconds.
*/
function throttle(callback, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
return callback(...args);
}
};
}
function concat$1(chars) {
let output = "";
for (let i = 0, len = chars.length; i < len; i++) output += chars[i];
return output;
}
/**
* Lazy value computed by the provided function.
*/
function lazy(fn) {
let isMemoized = false;
let memo;
const lazyValue = () => {
if (!isMemoized) {
memo = fn instanceof Function ? fn() : fn;
isMemoized = true;
}
return memo;
};
lazyValue.map = (callback) => lazy(() => callback(lazyValue()));
return lazyValue;
}
/**
* Find the next defined value after the given index in an array of strings. If there is no defined value
* after the index, return the closest defined value before the index. Return an empty string if no
* defined value was found.
*
*/
function findNextDefinedValue(arr, index) {
let value = arr.slice(index).find((val) => val);
if (!value) value = arr.slice(0, index).reverse().find((val) => val);
return value || "";
}
/** Get index of first header added by an ADD_COLUMNS_ROWS command */
function getAddHeaderStartIndex(position, base) {
return position === "after" ? base + 1 : base;
}
/**
* Compares n objects.
*/
function deepEquals(...o) {
if (o.length <= 1) return true;
for (let index = 1; index < o.length; index++) if (!_deepEquals(o[0], o[index])) return false;
return true;
}
function _deepEquals(o1, o2) {
if (o1 === o2) return true;
if (o1 && !o2 || o2 && !o1) return false;
if (typeof o1 !== typeof o2) return false;
if (typeof o1 !== "object") return false;
for (const key in o2) if (!(key in o1) && o2[key] !== void 0) return false;
for (const key in o1) {
if (typeof o1[key] !== typeof o2[key]) return false;
if (typeof o1[key] === "object") {
if (!_deepEquals(o1[key], o2[key])) return false;
} else if (o1[key] !== o2[key]) return false;
}
return true;
}
/**
* Compares two arrays.
* For performance reasons, this function is to be preferred
* to 'deepEquals' in the case we know that the inputs are arrays.
*/
function deepEqualsArray(arr1, arr2) {
if (arr1.length !== arr2.length) return false;
for (let i = 0; i < arr1.length; i++) if (!deepEquals(arr1[i], arr2[i])) return false;
return true;
}
/**
* Check if the given array contains all the values of the other array.
* It makes the assumption that both array do not contain duplicates.
*/
function includesAll(arr, values) {
if (arr.length < values.length) return false;
const set = new Set(arr);
return values.every((value) => set.has(value));
}
/**
* Return an object with all the keys in the object that have a falsy value removed.
*/
function removeFalsyAttributes(obj) {
if (!obj) return obj;
const cleanObject = { ...obj };
Object.keys(cleanObject).forEach((key) => !cleanObject[key] && delete cleanObject[key]);
return cleanObject;
}
/**
* Equivalent to "\s" in regexp, minus the new lines characters
*
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes
*/
const specialWhiteSpaceSpecialCharacters = [
" ",
"\f",
"\v",
String.fromCharCode(parseInt("00a0", 16)),
String.fromCharCode(parseInt("1680", 16)),
String.fromCharCode(parseInt("2000", 16)),
String.fromCharCode(parseInt("200a", 16)),
String.fromCharCode(parseInt("2028", 16)),
String.fromCharCode(parseInt("2029", 16)),
String.fromCharCode(parseInt("202f", 16)),
String.fromCharCode(parseInt("205f", 16)),
String.fromCharCode(parseInt("3000", 16)),
String.fromCharCode(parseInt("feff", 16))
];
const specialWhiteSpaceRegexp = new RegExp(specialWhiteSpaceSpecialCharacters.join("|"), "g");
const newLineRegexp = /(\r\n|\r)/g;
const whiteSpaceCharacters = specialWhiteSpaceSpecialCharacters.concat([" "]);
/**
* Replace all different newlines characters by \n.
*/
function replaceNewLines(text) {
if (!text) return "";
return text.replace(newLineRegexp, "\n");
}
/**
* Determine if the numbers are consecutive.
*/
function isConsecutive(iterable) {
const array = Array.from(iterable).sort((a, b) => a - b);
for (let i = 1; i < array.length; i++) if (array[i] - array[i - 1] !== 1) return false;
return true;
}
/**
* Creates a version of the function that's memoized on the value of its first argument, if any.
*/
function memoize(func) {
const cache = /* @__PURE__ */ new Map();
const funcName = func.name ? func.name + " (memoized)" : "memoized";
return { [funcName](...args) {
if (!cache.has(args[0])) cache.set(args[0], func(...args));
return cache.get(args[0]);
} }[funcName];
}
/**
* Removes the specified indexes from the array.
* Sparse (empty) elements are transformed to undefined (unless their index is explicitly removed).
*/
function removeIndexesFromArray(array, indexes) {
const toRemove = new Set(indexes);
const newArray = [];
for (let i = 0; i < array.length; i++) if (!toRemove.has(i)) newArray.push(array[i]);
return newArray;
}
function insertItemsAtIndex(array, items, index) {
return array.slice(0, index).concat(items).concat(array.slice(index));
}
function replaceItemAtIndex(array, newItem, index) {
const newArray = [...array];
newArray[index] = newItem;
return newArray;
}
function trimContent(content) {
return content.split("\n").map((line) => line.replace(/\s+/g, " ").trim()).join("\n");
}
function isNumberBetween(value, min, max) {
if (min > max) return isNumberBetween(value, max, min);
return value >= min && value <= max;
}
/**
* Get a Regex for the find & replace that matches the given search string and options.
*/
function getSearchRegex(searchStr, searchOptions) {
let searchValue = escapeRegExp(searchStr);
const flags = !searchOptions.matchCase ? "i" : "";
if (searchOptions.exactMatch) searchValue = `^${searchValue}$`;
return RegExp(searchValue, flags);
}
/**
* Alternative to Math.max that works with large arrays.
* Typically useful for arrays bigger than 100k elements.
*/
function largeMax(array) {
let len = array.length;
if (len < 1e5) return Math.max(...array);
let max = -Infinity;
while (len--) max = array[len] > max ? array[len] : max;
return max;
}
/**
* Alternative to Math.min that works with large arrays.
* Typically useful for arrays bigger than 100k elements.
*/
function largeMin(array) {
let len = array.length;
if (len < 1e5) return Math.min(...array);
let min = Infinity;
while (len--) min = array[len] < min ? array[len] : min;
return min;
}
var TokenizingChars = class {
text;
currentIndex = 0;
current;
constructor(text) {
this.text = text;
this.current = text[0];
}
shift() {
const current = this.current;
const next = this.text[++this.currentIndex];
this.current = next;
return current;
}
advanceBy(length) {
this.currentIndex += length;
this.current = this.text[this.currentIndex];
}
isOver() {
return this.currentIndex >= this.text.length;
}
remaining() {
return this.text.substring(this.currentIndex);
}
currentStartsWith(str) {
if (this.current !== str[0]) return false;
for (let j = 1; j < str.length; j++) if (this.text[this.currentIndex + j] !== str[j]) return false;
return true;
}
};
/**
* Remove duplicates from an array.
*
* @param array The array to remove duplicates from.
* @param cb A callback to get an element value.
*/
function removeDuplicates$1(array, cb = (a) => a) {
const set = /* @__PURE__ */ new Set();
return array.filter((item) => {
const key = cb(item);
if (set.has(key)) return false;
set.add(key);
return true;
});
}
/**
* Similar to transposing and array, but with POJOs instead of arrays. Useful, for example, when manipulating
* a POJO grid[col][row] and you want to transpose it to grid[row][col].
*
* The resulting object is created such as result[key1][key2] = pojo[key2][key1]
*/
function transpose2dPOJO(pojo) {
const result = {};
for (const key in pojo) for (const subKey in pojo[key]) {
if (!result[subKey]) result[subKey] = {};
result[subKey][key] = pojo[key][subKey];
}
return result;
}
function getUniqueText(text, texts, options = {}) {
const compute = options.compute ?? ((text, i) => `${text} (${i})`);
const computeFirstOne = options.computeFirstOne ?? false;
let i = options.start ?? 1;
let newText = computeFirstOne ? compute(text, i) : text;
while (texts.includes(newText)) newText = compute(text, i++);
return newText;
}
function isFormula(content) {
return content.startsWith("=") || content.startsWith("+");
}
function chartStyleToCellStyle(style) {
return {
bold: style.bold,
italic: style.italic,
fontSize: style.fontSize,
textColor: style.color,
align: style.align
};
}
function doesCellContainFunction(cell, formula) {
return cell.isFormula && cell.compiledFormula.usesSymbol(formula);
}
/** Return the number of cols/rows missing for result of the formula to be able to spread */
function getMissingHeadersForSpreadResult(getters, position, formula) {
const { sheetId, col, row } = position;
if (!isFormula(formula)) return;
const evaluated = getters.evaluateFormula(sheetId, formula, {
sheetId,
col,
row
});
if (!isMatrix(evaluated)) return;
const numberOfRows = getters.getNumberRows(sheetId);
const numberOfCols = getters.getNumberCols(sheetId);
return {
missingRows: row + evaluated[0].length - numberOfRows,
missingCols: col + evaluated.length - numberOfCols
};
}
//#endregion
//#region src/helpers/coordinates.ts
/**
* Convert a (col) number to the corresponding letter.
*
* Examples:
* 0 => 'A'
* 25 => 'Z'
* 26 => 'AA'
* 27 => 'AB'
*/
function numberToLetters(n) {
if (n < 0) throw new Error(`number must be positive. Got ${n}`);
if (n < 26) return String.fromCharCode(65 + n);
else return numberToLetters(Math.floor(n / 26) - 1) + numberToLetters(n % 26);
}
function lettersToNumber(letters) {
let result = 0;
const l = letters.length;
for (let i = 0; i < l; i++) {
const colIndex = charToNumber(letters[i]);
result = result * 26 + colIndex;
}
return result - 1;
}
function charToNumber(char) {
const charCode = char.charCodeAt(0);
return charCode >= 65 && charCode <= 90 ? charCode - 64 : charCode - 96;
}
function isCharALetter(char) {
return char >= "A" && char <= "Z" || char >= "a" && char <= "z";
}
function isCharADigit(char) {
return char >= "0" && char <= "9";
}
const MAX_COL = lettersToNumber("ZZZ");
const MAX_ROW = 9999998;
function consumeSpaces(chars) {
while (chars.current === " ") chars.advanceBy(1);
}
function consumeLetters(chars) {
if (chars.current === "$") chars.advanceBy(1);
if (!chars.current || !isCharALetter(chars.current)) return -1;
let colCoordinate = 0;
while (chars.current && isCharALetter(chars.current)) colCoordinate = colCoordinate * 26 + charToNumber(chars.shift());
return colCoordinate;
}
function consumeDigits(chars) {
if (chars.current === "$") chars.advanceBy(1);
if (!chars.current || !isCharADigit(chars.current)) return -1;
let num = 0;
while (chars.current && isCharADigit(chars.current)) num = num * 10 + Number(chars.shift());
return num;
}
/**
* Convert a "XC" coordinate to cartesian coordinates.
*
* Examples:
* A1 => [0,0]
* B3 => [1,2]
*
* Note: it also accepts lowercase coordinates, but not fixed references
*/
function toCartesian(xc) {
const chars = new TokenizingChars(xc);
consumeSpaces(chars);
const letterPart = consumeLetters(chars);
if (letterPart === -1 || !chars.current) throw new Error(`Invalid cell description: ${xc}`);
const num = consumeDigits(chars);
consumeSpaces(chars);
const col = letterPart - 1;
const row = num - 1;
if (!chars.isOver() || col > MAX_COL || row > 9999998) throw new Error(`Invalid cell description: ${xc}`);
return {
col,
row
};
}
/**
* Convert from cartesian coordinate to the "XC" coordinate system.
*
* Examples:
* - 0,0 => A1
* - 1,2 => B3
* - 0,0, {colFixed: false, rowFixed: true} => A$1
* - 1,2, {colFixed: true, rowFixed: false} => $B3
*/
function toXC(col, row, rangePart = {
colFixed: false,
rowFixed: false
}) {
return (rangePart.colFixed ? "$" : "") + numberToLetters(col) + (rangePart.rowFixed ? "$" : "") + String(row + 1);
}
//#endregion
//#region src/helpers/recompute_zones.ts
/**
* ####################################################
* # INTRODUCTION
* ####################################################
*
* This file contain the function recomputeZones.
* This function try to recompute in a performant way
* an ensemble of zones possibly overlapping to avoid
* overlapping and to reduce the number of zones.
*
* It also allows to remove some zones from the ensemble.
*
* In the following example, 2 zones are overlapping.
* Applying recomputeZones will return zones without
* overlapping:
*
* ["B3:D4", "D2:E3"] ["B3:C4", "D2:D4", "E2:E3"]
*
* A B C D E A B C D E
* 1 ___ 1 ___
* 2 ___|_ | 2 ___| | |
* 3 | |_|_| ---> 3 | | |_|
* 4 |_____| 4 |___|_|
* 6 6
* 7 7
*
*
* In the following example, 2 zones are contiguous.
* Applying recomputeZones will return only one zone:
*
* ["B2:B3", "C2:D3"] ["B2:D3"]
*
* A B C D E A B C D E
* 1 _ ___ 1 _____
* 2 | | | ---> 2 | |
* 3 |_|___| 3 |_____|
* 4 4
*
*
* In the following example, we want to remove a zone
* from the ensemble. Applying recomputeZones will
* return the ensemble without the zone to remove:
*
* remove ["C3:D3"] ["B2:B4", "C2:D2",
* "C4:D4", "E2:E4"]
*
* A B C D E F A B C D E F
* 1 _______ 1 _______
* 2 | | ---> 2 | |___| |
* 3 | xxx | 3 | |___| |
* 4 |_______| 4 |_|___|_|
* 5 5
*
*
* The exercise seems simple when we have only 2 zones.
* But with n zones and in a performant way, we want to
* avoid comparing each zone with all the others.
*
*
* ####################################################
* # Methodological approach
* ####################################################
*
* The methodological approach to avoid comparing each
* zone with all the others is to use a data structure
* that allow to quickly find which zones are
* overlapping with any other given zone.
*
* Here the idea is to profile the zones at the columns level.
*
* To do that, we propose to use a data structure
* composed of 2 parts:
* - profilesStartingPosition: a sorted number array
* indicating on which columns a new profile begins.
* - profiles: a map where the key is a column
* position (from profilesStartingPosition) and the
* value is a sorted number array representing a
* profile.
*
*
* See the following example: here profileStartingPosition
* corresponds to [A,C,E,G,K]
* A B C D E F G H I J K so with number [0,2,4,6,10]
* 1 ' ' ' '
* 2 ' ' '_______' here profile correspond
* 3 '___' |_______| for A to []
* 4 | | for C to [3, 5]
* 5 |___| for E to []
* 6 for G to [2, 3]
* 7 for K to []
*
*
* Now we can easily find which zones are overlapping
* with a given zone. Suppose we want to add a new zone
* D5:H6 to the ensemble:
*
* With a binary search of left and right
* A B C D E F G H I J K on profilesStartingPosition, we can
* 1 ' ' ' ' find the indexes of the profiles on which
* 2 ' ' '_______' to apply a modification.
* 3 '___' |_______|
* 4 | _|_______ Here we will:
* 5 |_|_| | - add a new profile in D --> become [3, 6]
* 6 |_________| - modify the profile in E --> become [4, 6]
* 7 - modify the profile in G --> become [2, 3, 4, 6]
* - add a new profile in I --> become [8, 10]
*
* See below the result:
*
* Note the particularity of the profile
* A B C D E F G H I J K for G: it will correspond to [2, 3, 4, 6]
* 1 ' ' ' ' ' '
* 2 ' ' ' '___'___' To know how to modify the profile (add a
* 3 '_'_' |___|___| zone or remove it) we do a binary
* 4 | | |___ ___ search of the top and bottom value on the
* 5 |_| | | | profile array. Depending on the result index
* 6 |_|___|___| parity (odd or even), because zone boundaries
* 7 go by pairs, we know if we are in a zone or
* not and how operate.
*/
/**
* Recompute the zone without the cells in toRemoveZones and avoid overlapping.
* This compute is particularly useful because after this function:
* - you will find coordinate of a cell only once among all the zones
* - the number of zones will be reduced to the minimum
*/
function recomputeZones(zones, zonesToRemove = []) {
if (zones.length <= 1 && zonesToRemove.length === 0) return zones;
const profilesStartingPosition = [0];
const profiles = new Map([[0, []]]);
modifyProfiles(profilesStartingPosition, profiles, zones, false);
modifyProfiles(profilesStartingPosition, profiles, zonesToRemove, true);
return constructZonesFromProfiles(profilesStartingPosition, profiles);
}
function modifyProfiles(profilesStartingPosition, profiles, zones, toRemove = false) {
for (const zone of zones) {
const leftValue = zone.left;
const rightValue = zone.right === void 0 ? void 0 : zone.right + 1;
const leftIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, leftValue, true, 0);
const rightIndex = findIndexAndCreateProfile(profilesStartingPosition, profiles, rightValue, false, leftIndex);
for (let i = leftIndex; i <= rightIndex; i++) modifyProfile(profiles.get(profilesStartingPosition[i]), zone, toRemove);
removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex);
}
}
function profilesContainsZone(profilesStartingPosition, profiles, zone) {
const leftValue = zone.left;
const rightValue = zone.right;
const leftIndex = binaryPredecessorSearch(profilesStartingPosition, leftValue, 0);
const rightIndex = rightValue === void 0 ? profilesStartingPosition.length - 1 : binaryPredecessorSearch(profilesStartingPosition, rightValue, leftIndex);
/**
* The `profilesStartingPosition` array always contains at least the value `0` at its first position,
* ensuring that applying `binaryPredecessorSearch` will always return a valid index.
* Therefore, it is not necessary to check if the result of `binaryPredecessorSearch` equals `-1`.
*/
const topValue = zone.top;
const bottomValue = zone.bottom === void 0 ? void 0 : zone.bottom + 1;
for (let i = leftIndex; i <= rightIndex; i++) {
const profile = profiles.get(profilesStartingPosition[i]);
const topPredIndex = binaryPredecessorSearch(profile, topValue, 0);
if (topPredIndex === -1 || topPredIndex % 2 !== 0) return false;
const bottomSuccIndex = bottomValue === void 0 ? profile.length : binarySuccessorSearch(profile, bottomValue, 0);
if (topPredIndex + 1 !== bottomSuccIndex) return false;
}
return true;
}
function findIndexAndCreateProfile(profilesStartingPosition, profiles, value, searchLeft, startIndex) {
if (value === void 0) return profilesStartingPosition.length - 1;
const predecessorIndex = binaryPredecessorSearch(profilesStartingPosition, value, startIndex);
if (value !== profilesStartingPosition[predecessorIndex]) {
profilesStartingPosition.splice(predecessorIndex + 1, 0, value);
profiles.set(value, [...profiles.get(profilesStartingPosition[predecessorIndex])]);
return searchLeft ? predecessorIndex + 1 : predecessorIndex;
}
return searchLeft ? predecessorIndex : predecessorIndex - 1;
}
/**
* Suppose the following Suppose we want to add We want to have the
* profile: the following zone: following profile:
*
* A B C D E F A B C D E F A B C D E F
* 1 '___' 1 ' ' 1 '___'
* 2 |___| 2 '___' 2 | |
* 3 ' ' 3 | | 3 | |
* 4 '___' --> 4 | | --> 4 | |
* 6 | | 6 |___| 6 | |
* 7 |___| 7 7 |___|
* 8 8 8
*
* the profile for 'C' the top zone correspond Here [2, 3, 5, 8] with [3, 7]
* corresponds to: to 3 and the bottom zone would be merged into [2, 8]
* ____ ____ correspond to 6
* [2, 3, 5, 8] would be the profile: The difficulty of modify profile
* ____ is to know what must be deleted
* Note that the 'filled [3, 7] and what must be added to the
* zone' are always between existing profile.
* an even index and its
* next index
*
*/
function modifyProfile(profile, zone, toRemove = false) {
const topValue = zone.top;
const bottomValue = zone.bottom === void 0 ? void 0 : zone.bottom + 1;
const newPoints = [];
const topPredIndex = binaryPredecessorSearch(profile, topValue, 0, false);
if (topPredIndex % 2 !== 0 && !toRemove || topPredIndex % 2 === 0 && toRemove) newPoints.push(topValue);
if (bottomValue === void 0) {
profile.splice(topPredIndex + 1);
profile.push(...newPoints);
return;
}
const bottomSuccIndex = binarySuccessorSearch(profile, bottomValue, 0, false);
if (bottomSuccIndex % 2 === 0 && !toRemove || bottomSuccIndex % 2 !== 0 && toRemove) newPoints.push(bottomValue);
const toDelete = bottomSuccIndex - topPredIndex - 1;
const toInsert = newPoints.length;
const start = topPredIndex + 1;
if (start === profile.length - 1 && toDelete === 1 && toInsert === 1) profile[start] = newPoints[0] ?? newPoints[1];
else profile.splice(start, toDelete, ...newPoints);
}
function removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex) {
const start = leftIndex - 1 === -1 ? 0 : leftIndex - 1;
const end = rightIndex === profilesStartingPosition.length - 1 ? rightIndex : rightIndex + 1;
for (let i = end; i > start; i--) if (deepEqualsArray(profiles.get(profilesStartingPosition[i]), profiles.get(profilesStartingPosition[i - 1]))) {
profiles.delete(profilesStartingPosition[i]);
profilesStartingPosition.splice(i, 1);
}
}
function constructZonesFromProfiles(profilesStartingPosition, profiles) {
const mergedZone = [];
let pendingZones = [];
for (let colIndex = 0; colIndex < profilesStartingPosition.length; colIndex++) {
const left = profilesStartingPosition[colIndex];
const profile = profiles.get(left);
if (!profile || profile.length === 0) {
mergedZone.push(...pendingZones);
pendingZones = [];
continue;
}
let right = profilesStartingPosition[colIndex + 1];
if (right !== void 0) right--;
const nextPendingZones = [];
for (let i = 0; i < profile.length; i += 2) {
const top = profile[i];
let bottom = profile[i + 1];
if (bottom !== void 0) bottom--;
const profileZone = {
top,
left,
bottom,
right
};
if (bottom === void 0 && top !== 0 || right === void 0 && left !== 0) profileZone.hasHeader = true;
let findCorrespondingZone = false;
for (let j = pendingZones.length - 1; j >= 0; j--) {
const pendingZone = pendingZones[j];
if (pendingZone.top === profileZone.top && pendingZone.bottom === profileZone.bottom) {
pendingZone.right = profileZone.right;
pendingZones.splice(j, 1);
nextPendingZones.push(pendingZone);
findCorrespondingZone = true;
break;
}
}
if (!findCorrespondingZone) nextPendingZones.push(profileZone);
}
mergedZone.push(...pendingZones);
pendingZones = nextPendingZones;
}
mergedZone.push(...pendingZones);
return mergedZone;
}
function binaryPredecessorSearch(arr, val, start = 0, matchEqual = true) {
let end = arr.length - 1;
let result = -1;
while (start <= end) {
const mid = start + (end - start >> 1);
if (arr[mid] === val && matchEqual) return mid;
else if (arr[mid] < val) {
result = mid;
start = mid + 1;
} else end = mid - 1;
}
return result;
}
function binarySuccessorSearch(arr, val, start = 0, matchEqual = true) {
let end = arr.length - 1;
let result = arr.length;
while (start <= end) {
const mid = start + (end - start >> 1);
if (arr[mid] === val && matchEqual) return mid;
else if (arr[mid] > val) {
result = mid;
end = mid - 1;
} else start = mid + 1;
}
return result;
}
//#endregion
//#region src/helpers/zones.ts
/**
* Convert from a cartesian reference to a Zone
* The range boundaries will be kept in the same order as the
* ones in the text.
* Examples:
* "A1" ==> Top 0, Bottom 0, Left: 0, Right: 0
* "B1:B3" ==> Top 0, Bottom 3, Left: 1, Right: 1
* "Sheet1!A1" ==> Top 0, Bottom 0, Left: 0, Right: 0
* "Sheet1!B1:B3" ==> Top 0, Bottom 3, Left: 1, Right: 1
* "C3:A1" ==> Top 2, Bottom 0, Left 2, Right 0
* "A:A" ==> Top 0, Bottom undefined, Left 0, Right 0
* "A:B3" or "B3:A" ==> Top 2, Bottom undefined, Left 0, Right 1
*
* @param xc the string reference to convert
*
*/
function toZoneWithoutBoundaryChanges(xc) {
const chars = new TokenizingChars(xc);
consumeSpaces(chars);
const sheetSeparatorIndex = xc.indexOf("!");
if (sheetSeparatorIndex !== -1) chars.advanceBy(sheetSeparatorIndex + 1);
const leftLetters = consumeLetters(chars);
const leftNumbers = consumeDigits(chars);
let top, bottom, left, right;
let fullCol = false;
let fullRow = false;
let hasHeader = false;
if (leftNumbers === -1) {
left = right = leftLetters - 1;
top = bottom = 0;
fullCol = true;
} else if (leftLetters === -1) {
top = bottom = leftNumbers - 1;
left = right = 0;
fullRow = true;
} else {
left = right = leftLetters - 1;
top = bottom = leftNumbers - 1;
hasHeader = true;
}
consumeSpaces(chars);
if (chars.current === ":") {
chars.advanceBy(1);
consumeSpaces(chars);
const rightLetters = consumeLetters(chars);
const rightNumbers = consumeDigits(chars);
if (rightNumbers === -1) {
right = rightLetters - 1;
fullCol = true;
} else if (rightLetters === -1) {
bottom = rightNumbers - 1;
fullRow = true;
} else {
right = rightLetters - 1;
bottom = rightNumbers - 1;
top = fullCol ? bottom : top;
left = fullRow ? right : left;
hasHeader = true;
}
}
const zone = {
top,
left,
bottom: fullCol ? void 0 : bottom,
right: fullRow ? void 0 : right
};
hasHeader = hasHeader && (fullRow || fullCol);
if (hasHeader) zone.hasHeader = hasHeader;
return zone;
}
/**
* Convert from a cartesian reference to a (possibly unbounded) Zone
*
* Examples:
* "A1" ==> Top 0, Bottom 0, Left: 0, Right: 0
* "B1:B3" ==> Top 0, Bottom 3, Left: 1, Right: 1
* "B:B" ==> Top 0, Bottom undefined, Left: 1, Right: 1
* "B2:B" ==> Top 1, Bottom undefined, Left: 1, Right: 1, hasHeader: 1
* "Sheet1!A1" ==> Top 0, Bottom 0, Left: 0, Right: 0
* "Sheet1!B1:B3" ==> Top 0, Bottom 3, Left: 1, Right: 1
*
* @param xc the string reference to convert
*
*/
function toUnboundedZone(xc) {
const orderedZone = reorderZone(toZoneWithoutBoundaryChanges(xc));
const bottom = orderedZone.bottom;
const right = orderedZone.right;
if (bottom !== void 0 && bottom > 9999998 || right !== void 0 && right > MAX_COL) throw new Error(`Range string out of bounds: ${xc}`);
if (bottom === void 0 && right === void 0) throw new Error("Wrong zone xc. The zone cannot be at the same time a full column and a full row");
return orderedZone;
}
/**
* Convert from a cartesian reference to a Zone.
* Will return throw an error if given a unbounded zone (eg : A:A).
*
* Examples:
* "A1" ==> Top 0, Bottom 0, Left: 0, Right: 0
* "B1:B3" ==> Top 0, Bottom 2, Left: 1, Right: 1
* "Sheet1!A1" ==> Top 0, Bottom 0, Left: 0, Right: 0
* "Sheet1!B1:B3" ==> Top 0, Bottom 2, Left: 1, Right: 1
*
* @param xc the string reference to convert
*
*/
function toZone(xc) {
const zone = toUnboundedZone(xc);
if (zone.bottom === void 0 || zone.right === void 0) throw new Error("This does not support unbounded ranges");
return zone;
}
function isXcValid(xc) {
return isZoneValid(toUnboundedZone(xc));
}
/**
* Check that the given string is a correct xc representation (ie a valid zone). The try-catch
* added over the ixXcValid call is necessary because the function can throw an error if the
* string is not convertible to a zone by the toUnboundedZone function.
*/
function isXcRepresentation(xc) {
try {
return isXcValid(xc);
} catch (e) {
return false;
}
}
/**
* Check that the zone has valid coordinates and in
* the correct order.
*/
function isZoneValid(zone) {
const { bottom, top, left, right } = zone;
if (bottom !== void 0 && isNaN(bottom) || isNaN(top) || isNaN(left) || right !== void 0 && isNaN(right)) return false;
return isZoneOrdered(zone) && zone.top >= 0 && zone.left >= 0;
}
/**
* Check that the zone properties are in the correct order.
*/
function isZoneOrdered(zone) {
return (zone.bottom === void 0 || zone.bottom >= zone.top && zone.bottom >= 0) && (zone.right === void 0 || zone.right >= zone.left && zone.right >= 0);
}
/**
* Convert from zone to a cartesian reference
*
*/
function zoneToXc(zone) {
const { top, bottom, left, right } = zone;
const hasHeader = "hasHeader" in zone ? zone.hasHeader : false;
const isOneCell = top === bottom && left === right;
if (bottom === void 0 && right !== void 0) return top === 0 && !hasHeader ? `${numberToLetters(left)}:${numberToLetters(right)}` : `${toXC(left, top)}:${numberToLetters(right)}`;
else if (right === void 0 && bottom !== void 0) return left === 0 && !hasHeader ? `${top + 1}:${bottom + 1}` : `${toXC(left, top)}:${bottom + 1}`;
else if (bottom !== void 0 && right !== void 0) return isOneCell ? toXC(left, top) : `${toXC(left, top)}:${toXC(right, bottom)}`;
throw new Error("Bad zone format");
}
/**
* Expand a z