diff --git a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js index 5c07f5597fb..8f5a007c73f 100644 --- a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js +++ b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js @@ -2,9 +2,9 @@ /** * This file is generated by o-spreadsheet build tools. Do not edit it. * @see https://github.com/odoo/o-spreadsheet - * @version 16.5.0-alpha.12 - * @date 2023-10-20T15:39:44.984Z - * @hash 430f0907 + * @version 17.0.1 + * @date 2023-11-02T09:48:14.706Z + * @hash 0887d5bb5 */ (function (exports, owl) { @@ -1983,6 +1983,7 @@ CommandResult["InvalidFigureId"] = "InvalidFigureId"; CommandResult["InputAlreadyFocused"] = "InputAlreadyFocused"; CommandResult["MaximumRangesReached"] = "MaximumRangesReached"; + CommandResult["MinimumRangesReached"] = "MinimumRangesReached"; CommandResult["InvalidChartDefinition"] = "InvalidChartDefinition"; CommandResult["InvalidDataSet"] = "InvalidDataSet"; CommandResult["InvalidLabelRange"] = "InvalidLabelRange"; @@ -2518,7 +2519,7 @@ function visitMatchingRanges(args, cb, locale, isQuery = false) { const countArg = args.length; if (countArg % 2 === 1) { - throw new Error(_t(`Function [[FUNCTION_NAME]] expects criteria_range and criterion to be in pairs.`)); + throw new Error(_t("Function [[FUNCTION_NAME]] expects criteria_range and criterion to be in pairs.")); } const dimRow = args[0].length; const dimCol = args[0][0].length; @@ -2528,7 +2529,7 @@ if (!isMatrix(criteriaRange) || criteriaRange.length !== dimRow || criteriaRange[0].length !== dimCol) { - throw new Error(_t(`Function [[FUNCTION_NAME]] expects criteria_range to have the same dimension`)); + throw new Error(_t("Function [[FUNCTION_NAME]] expects criteria_range to have the same dimension")); } const description = toString(args[i + 1]); predicates.push(getPredicate(description, isQuery, locale)); @@ -3580,10 +3581,14 @@ (right !== undefined && 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 === undefined || (zone.bottom >= zone.top && zone.bottom >= 0)) && - (zone.right === undefined || (zone.right >= zone.left && zone.right >= 0)) && - zone.top >= 0 && - zone.left >= 0); + (zone.right === undefined || (zone.right >= zone.left && zone.right >= 0))); } /** * Convert from zone to a cartesian reference @@ -4131,6 +4136,9 @@ * If it's not the case, simply invert them, and invert the linked parts */ orderZone() { + if (isZoneOrdered(this._zone)) { + return this; + } const zone = { ...this._zone }; let parts = this.parts; if (zone.right !== undefined && zone.right < zone.left) { @@ -7282,8277 +7290,6 @@ onClosed: { type: Function, optional: true }, }; - //------------------------------------------------------------------------------ - // Arg description DSL - //------------------------------------------------------------------------------ - const ARG_REGEXP = /(.*?)\((.*?)\)(.*)/; - const ARG_TYPES = [ - "ANY", - "BOOLEAN", - "DATE", - "NUMBER", - "STRING", - "RANGE", - "RANGE", - "RANGE", - "RANGE", - "RANGE", - "META", - ]; - function arg(definition, description = "") { - return makeArg(definition, description); - } - function makeArg(str, description) { - let parts = str.match(ARG_REGEXP); - let name = parts[1].trim(); - let types = []; - let isOptional = false; - let isRepeating = false; - let isLazy = false; - let defaultValue; - for (let param of parts[2].split(",")) { - const key = param.trim().toUpperCase(); - let type = ARG_TYPES.find((t) => key === t); - if (type) { - types.push(type); - } - else if (key === "RANGE") { - types.push("RANGE"); - } - else if (key === "OPTIONAL") { - isOptional = true; - } - else if (key === "REPEATING") { - isRepeating = true; - } - else if (key === "LAZY") { - isLazy = true; - } - else if (key.startsWith("DEFAULT=")) { - defaultValue = param.trim().slice(8); - } - } - const result = { - name, - description, - type: types, - }; - if (isOptional) { - result.optional = true; - } - if (isRepeating) { - result.repeating = true; - } - if (isLazy) { - result.lazy = true; - } - if (defaultValue !== undefined) { - result.default = true; - result.defaultValue = defaultValue; - } - return result; - } - /** - * This function adds on description more general information derived from the - * arguments. - * - * This information is useful during compilation. - */ - function addMetaInfoFromArg(addDescr) { - let countArg = 0; - let minArg = 0; - let repeatingArg = 0; - for (let arg of addDescr.args) { - countArg++; - if (!arg.optional && !arg.repeating && !arg.default) { - minArg++; - } - if (arg.repeating) { - repeatingArg++; - } - } - const descr = addDescr; - descr.minArgRequired = minArg; - descr.maxArgPossible = repeatingArg ? Infinity : countArg; - descr.nbrArgRepeating = repeatingArg; - descr.getArgToFocus = argTargeting(countArg, repeatingArg); - descr.hidden = addDescr.hidden || false; - return descr; - } - /** - * Returns a function allowing finding which argument corresponds a position - * in a function. This is particularly useful for functions with repeatable - * arguments. - * - * Indeed the function makes it possible to etablish corespondance between - * arguments when the number of arguments supplied is greater than the number of - * arguments defined by the function. - * - * Ex: - * - * in the formula "=SUM(11, 55, 66)" which is defined like this "SUM(value1, [value2, ...])" - * - 11 corresponds to the value1 argument => position will be 1 - * - 55 corresponds to the [value2, ...] argument => position will be 2 - * - 66 corresponds to the [value2, ...] argument => position will be 2 - * - * in the formula "=AVERAGE.WEIGHTED(1, 2, 3, 4, 5, 6)" which is defined like this - * "AVERAGE.WEIGHTED(values, weights, [additional_values, ...], [additional_weights, ...])" - * - 1 corresponds to the values argument => position will be 1 - * - 2 corresponds to the weights argument => position will be 2 - * - 3 corresponds to the [additional_values, ...] argument => position will be 3 - * - 4 corresponds to the [additional_weights, ...] argument => position will be 4 - * - 5 corresponds to the [additional_values, ...] argument => position will be 3 - * - 6 corresponds to the [additional_weights, ...] argument => position will be 4 - */ - function argTargeting(countArg, repeatingArg) { - if (!repeatingArg) { - return (argPosition) => argPosition; - } - if (repeatingArg === 1) { - return (argPosition) => Math.min(argPosition, countArg); - } - const argBeforeRepeat = countArg - repeatingArg; - return (argPosition) => { - if (argPosition <= argBeforeRepeat) { - return argPosition; - } - const argAfterRepeat = (argPosition - argBeforeRepeat) % repeatingArg || repeatingArg; - return argBeforeRepeat + argAfterRepeat; - }; - } - //------------------------------------------------------------------------------ - // Argument validation - //------------------------------------------------------------------------------ - function validateArguments(args) { - let previousArgRepeating = false; - let previousArgOptional = false; - let previousArgDefault = false; - for (let current of args) { - if (current.type.includes("META") && current.type.length > 1) { - throw new Error(_t("Function ${name} has an argument that has been declared with more than one type whose type 'META'. The 'META' type can only be declared alone.")); - } - if (previousArgRepeating && !current.repeating) { - throw new Error(_t("Function ${name} has no-repeatable arguments declared after repeatable ones. All repeatable arguments must be declared last.")); - } - const previousIsOptional = previousArgOptional || previousArgRepeating || previousArgDefault; - const currentIsntOptional = !(current.optional || current.repeating || current.default); - if (previousIsOptional && currentIsntOptional) { - throw new Error(_t("Function ${name} has at mandatory arguments declared after optional ones. All optional arguments must be after all mandatory arguments.")); - } - previousArgRepeating = current.repeating; - previousArgOptional = current.optional; - previousArgDefault = current.default; - } - } - - function assertSingleColOrRow(errorStr, arg) { - assert(() => arg.length === 1 || arg[0].length === 1, errorStr); - } - function assertSameDimensions(errorStr, ...args) { - if (args.every(isMatrix)) { - const cols = args[0].length; - const rows = args[0][0].length; - for (const arg of args) { - assert(() => arg.length === cols && arg[0].length === rows, errorStr); - } - return; - } - if (args.some((arg) => Array.isArray(arg) && (arg.length !== 1 || arg[0].length !== 1))) { - throw new Error(errorStr); - } - } - function assertPositive(errorStr, arg) { - assert(() => arg > 0, errorStr); - } - function assertSquareMatrix(errorStr, arg) { - assert(() => arg.length === arg[0].length, errorStr); - } - function isNumberMatrix(arg) { - return arg.every((row) => row.every((val) => typeof val === "number")); - } - - function getUnitMatrix(n) { - const matrix = Array(n); - for (let i = 0; i < n; i++) { - matrix[i] = Array(n).fill(0); - matrix[i][i] = 1; - } - return matrix; - } - /** - * Invert a matrix and compute its determinant using Gaussian Elimination. - * - * The Matrix should be a square matrix, and should be indexed [col][row] instead of the - * standard mathematical indexing [row][col]. - */ - function invertMatrix(M) { - // Use Gaussian Elimination to calculate the inverse: - // (1) 'augment' the matrix (left) by the identity (on the right) - // (2) Turn the matrix on the left into the identity using elementary row operations - // (3) The matrix on the right becomes the inverse (was the identity matrix) - // - // There are 3 elementary row operations: - // (a) Swap 2 rows. This multiply the determinant by -1. - // (b) Multiply a row by a scalar. This multiply the determinant by that scalar. - // (c) Add to a row a multiple of another row. This does not change the determinant. - if (M.length !== M[0].length) { - throw new Error(`Function [[FUNCTION_NAME]] invert matrix error, only square matrices are invertible`); - } - let determinant = 1; - const dim = M.length; - const I = getUnitMatrix(dim); - const C = M.map((row) => row.slice()); - // Perform elementary row operations - for (let pivot = 0; pivot < dim; pivot++) { - let diagonalElement = C[pivot][pivot]; - // if we have a 0 on the diagonal we'll need to swap with a lower row - if (diagonalElement === 0) { - //look through every row below the i'th row - for (let row = pivot + 1; row < dim; row++) { - //if the ii'th row has a non-0 in the i'th col, swap it with that row - if (C[pivot][row] != 0) { - swapMatrixRows(C, pivot, row); - swapMatrixRows(I, pivot, row); - determinant *= -1; - break; - } - } - diagonalElement = C[pivot][pivot]; - //if it's still 0, matrix isn't invertible - if (diagonalElement === 0) { - return { determinant: 0 }; - } - } - // Scale this row down by e (so we have a 1 on the diagonal) - for (let col = 0; col < dim; col++) { - C[col][pivot] = C[col][pivot] / diagonalElement; - I[col][pivot] = I[col][pivot] / diagonalElement; - } - determinant *= diagonalElement; - // Subtract a multiple of the current row from ALL of - // the other rows so that there will be 0's in this column in the - // rows above and below this one - for (let row = 0; row < dim; row++) { - if (row === pivot) { - continue; - } - // We want to change this element to 0 - const e = C[pivot][row]; - // Subtract (the row above(or below) scaled by e) from (the - // current row) but start at the i'th column and assume all the - // stuff left of diagonal is 0 (which it should be if we made this - // algorithm correctly) - for (let col = 0; col < dim; col++) { - C[col][row] -= e * C[col][pivot]; - I[col][row] -= e * I[col][pivot]; - } - } - } - // We've done all operations, C should be the identity matrix I should be the inverse - return { inverted: I, determinant }; - } - function swapMatrixRows(matrix, row1, row2) { - for (let i = 0; i < matrix.length; i++) { - const tmp = matrix[i][row1]; - matrix[i][row1] = matrix[i][row2]; - matrix[i][row2] = tmp; - } - } - /** - * Matrix multiplication of 2 matrices. - * ex: matrix1 : n x l, matrix2 : m x n => result : m x l - * - * Note: we use indexing [col][row] instead of the standard mathematical notation [row][col] - */ - function multiplyMatrices(matrix1, matrix2) { - if (matrix1.length !== matrix2[0].length) { - throw new Error(_t("Cannot multiply matrices : incompatible matrices size.")); - } - const rowsM1 = matrix1[0].length; - const colsM2 = matrix2.length; - const n = matrix1.length; - const result = Array(colsM2); - for (let col = 0; col < colsM2; col++) { - result[col] = Array(rowsM1); - for (let row = 0; row < rowsM1; row++) { - let sum = 0; - for (let k = 0; k < n; k++) { - sum += matrix1[k][row] * matrix2[col][k]; - } - result[col][row] = sum; - } - } - return result; - } - - // ----------------------------------------------------------------------------- - // ARRAY_CONSTRAIN - // ----------------------------------------------------------------------------- - const ARRAY_CONSTRAIN = { - description: _t("Returns a result array constrained to a specific width and height."), - args: [ - arg("input_range (any, range)", _t("The range to constrain.")), - arg("rows (number)", _t("The number of rows in the constrained array.")), - arg("columns (number)", _t("The number of columns in the constrained array.")), - ], - returns: ["RANGE"], - computeValueAndFormat: function (array, rows, columns) { - const _array = toMatrix(array); - const _rowsArg = toInteger(rows?.value, this.locale); - const _columnsArg = toInteger(columns?.value, this.locale); - assertPositive(_t("The rows argument (%s) must be strictly positive.", _rowsArg.toString()), _rowsArg); - assertPositive(_t("The columns argument (%s) must be strictly positive.", _rowsArg.toString()), _columnsArg); - const _nbRows = Math.min(_rowsArg, _array[0].length); - const _nbColumns = Math.min(_columnsArg, _array.length); - return generateMatrix(_nbColumns, _nbRows, (col, row) => _array[col][row]); - }, - isExported: false, - }; - // ----------------------------------------------------------------------------- - // CHOOSECOLS - // ----------------------------------------------------------------------------- - const CHOOSECOLS = { - description: _t("Creates a new array from the selected columns in the existing range."), - args: [ - arg("array (any, range)", _t("The array that contains the columns to be returned.")), - arg("col_num (number, range)", _t("The first column index of the columns to be returned.")), - arg("col_num2 (number, range, repeating)", _t("The columns indexes of the columns to be returned.")), - ], - returns: ["RANGE"], - computeValueAndFormat: function (array, ...columns) { - const _array = toMatrix(array); - const _columns = flattenRowFirst(columns, (item) => toInteger(item?.value, this.locale)); - assert(() => _columns.every((col) => col > 0 && col <= _array.length), _t("The columns arguments must be between 1 and %s (got %s).", _array.length.toString(), (_columns.find((col) => col <= 0 || col > _array.length) || 0).toString())); - const result = Array(_columns.length); - for (let col = 0; col < _columns.length; col++) { - const colIndex = _columns[col] - 1; // -1 because columns arguments are 1-indexed - result[col] = _array[colIndex]; - } - return result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // CHOOSEROWS - // ----------------------------------------------------------------------------- - const CHOOSEROWS = { - description: _t("Creates a new array from the selected rows in the existing range."), - args: [ - arg("array (any, range)", _t("The array that contains the rows to be returned.")), - arg("row_num (number, range)", _t("The first row index of the rows to be returned.")), - arg("row_num2 (number, range, repeating)", _t("The rows indexes of the rows to be returned.")), - ], - returns: ["RANGE"], - computeValueAndFormat: function (array, ...rows) { - const _array = toMatrix(array); - const _rows = flattenRowFirst(rows, (item) => toInteger(item?.value, this.locale)); - const _nbColumns = _array.length; - assert(() => _rows.every((row) => row > 0 && row <= _array[0].length), _t("The rows arguments must be between 1 and %s (got %s).", _array[0].length.toString(), (_rows.find((row) => row <= 0 || row > _array[0].length) || 0).toString())); - return generateMatrix(_nbColumns, _rows.length, (col, row) => _array[col][_rows[row] - 1]); // -1 because rows arguments are 1-indexed - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // EXPAND - // ----------------------------------------------------------------------------- - const EXPAND = { - description: _t("Expands or pads an array to specified row and column dimensions."), - args: [ - arg("array (any, range)", _t("The array to expand.")), - arg("rows (number)", _t("The number of rows in the expanded array. If missing, rows will not be expanded.")), - arg("columns (number, optional)", _t("The number of columns in the expanded array. If missing, columns will not be expanded.")), - arg("pad_with (any, default=0)", _t("The value with which to pad.")), // @compatibility: on Excel, pad with #N/A - ], - returns: ["RANGE"], - computeValueAndFormat: function (arg, rows, columns, padWith = { value: 0 } // TODO : Replace with #N/A errors once it's supported - ) { - const _array = toMatrix(arg); - const _nbRows = toInteger(rows?.value, this.locale); - const _nbColumns = columns !== undefined ? toInteger(columns.value, this.local) : _array.length; - assert(() => _nbRows >= _array[0].length, _t("The rows arguments (%s) must be greater or equal than the number of rows of the array.", _nbRows.toString())); - assert(() => _nbColumns >= _array.length, _t("The columns arguments (%s) must be greater or equal than the number of columns of the array.", _nbColumns.toString())); - return generateMatrix(_nbColumns, _nbRows, (col, row) => col >= _array.length || row >= _array[col].length ? padWith : _array[col][row]); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // FLATTEN - // ----------------------------------------------------------------------------- - const FLATTEN = { - description: _t("Flattens all the values from one or more ranges into a single column."), - args: [ - arg("range (any, range)", _t("The first range to flatten.")), - arg("range2 (any, range, repeating)", _t("Additional ranges to flatten.")), - ], - returns: ["RANGE"], - computeValueAndFormat: function (...ranges) { - return [flattenRowFirst(ranges, (val) => (val === undefined ? { value: "" } : val))]; - }, - isExported: false, - }; - // ----------------------------------------------------------------------------- - // FREQUENCY - // ----------------------------------------------------------------------------- - const FREQUENCY = { - description: _t("Calculates the frequency distribution of a range."), - args: [ - arg("data (range)", _t("The array of ranges containing the values to be counted.")), - arg("classes (number, range)", _t("The range containing the set of classes.")), - ], - returns: ["RANGE"], - compute: function (data, classes) { - const _data = flattenRowFirst([data], (val) => val).filter((val) => typeof val === "number"); - const _classes = flattenRowFirst([classes], (val) => val).filter((val) => typeof val === "number"); - /** - * Returns the frequency distribution of the data in the classes, ie. the number of elements in the range - * between each classes. - * - * For example: - * - data = [1, 3, 2, 5, 4] - * - classes = [3, 5, 1] - * - * The result will be: - * - 2 ==> number of elements 1 > el >= 3 - * - 2 ==> number of elements 3 > el >= 5 - * - 1 ==> number of elements <= 1 - * - 0 ==> number of elements > 5 - * - * @compatibility: GSheet sort the input classes. We do the implemntation of Excel, where we kee the classes unsorted. - */ - const sortedClasses = _classes - .map((value, index) => ({ initialIndex: index, value, count: 0 })) - .sort((a, b) => a.value - b.value); - sortedClasses.push({ initialIndex: sortedClasses.length, value: Infinity, count: 0 }); - const sortedData = _data.sort((a, b) => a - b); - let index = 0; - for (const val of sortedData) { - while (val > sortedClasses[index].value && index < sortedClasses.length - 1) { - index++; - } - sortedClasses[index].count++; - } - const result = sortedClasses - .sort((a, b) => a.initialIndex - b.initialIndex) - .map((val) => val.count); - return [result]; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // HSTACK - // ----------------------------------------------------------------------------- - const HSTACK = { - description: _t("Appends ranges horizontally and in sequence to return a larger array."), - args: [ - arg("range1 (any, range)", _t("The first range to be appended.")), - arg("range2 (any, range, repeating)", _t("Additional ranges to add to range1.")), - ], - returns: ["RANGE"], - computeValueAndFormat: function (...ranges) { - const nbRows = Math.max(...ranges.map((r) => r?.[0]?.length ?? 0)); - const result = []; - for (const range of ranges) { - const _range = toMatrix(range); - for (let col = 0; col < _range.length; col++) { - //TODO: fill with #N/A for unavailable values instead of zeroes - const array = Array(nbRows).fill({ value: null }); - for (let row = 0; row < _range[col].length; row++) { - array[row] = _range[col][row]; - } - result.push(array); - } - } - return result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MDETERM - // ----------------------------------------------------------------------------- - const MDETERM = { - description: _t("Returns the matrix determinant of a square matrix."), - args: [ - arg("square_matrix (number, range)", _t("An range with an equal number of rows and columns representing a matrix whose determinant will be calculated.")), - ], - returns: ["NUMBER"], - compute: function (matrix) { - const _matrix = toMatrix(matrix); - assertSquareMatrix(_t("The argument square_matrix must have the same number of columns and rows."), _matrix); - if (!isNumberMatrix(_matrix)) { - throw new Error(_t("The argument square_matrix must be a matrix of numbers.")); - } - const { determinant } = invertMatrix(_matrix); - return determinant; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MINVERSE - // ----------------------------------------------------------------------------- - const MINVERSE = { - description: _t("Returns the multiplicative inverse of a square matrix."), - args: [ - arg("square_matrix (number, range)", _t("An range with an equal number of rows and columns representing a matrix whose multiplicative inverse will be calculated.")), - ], - returns: ["RANGE"], - compute: function (matrix) { - const _matrix = toMatrix(matrix); - assertSquareMatrix(_t("The argument square_matrix must have the same number of columns and rows."), _matrix); - if (!isNumberMatrix(_matrix)) { - throw new Error(_t("The argument square_matrix must be a matrix of numbers.")); - } - const { inverted } = invertMatrix(_matrix); - if (!inverted) { - throw new Error(_t("The matrix is not invertible.")); - } - return inverted; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MMULT - // ----------------------------------------------------------------------------- - const MMULT = { - description: _t("Calculates the matrix product of two matrices."), - args: [ - arg("matrix1 (number, range)", _t("The first matrix in the matrix multiplication operation.")), - arg("matrix2 (number, range)", _t("The second matrix in the matrix multiplication operation.")), - ], - returns: ["RANGE"], - compute: function (matrix1, matrix2) { - const _matrix1 = toMatrix(matrix1); - const _matrix2 = toMatrix(matrix2); - assert(() => _matrix1.length === _matrix2[0].length, _t("In [[FUNCTION_NAME]], the number of columns of the first matrix (%s) must be equal to the \ - number of rows of the second matrix (%s).", _matrix1.length.toString(), _matrix2[0].length.toString())); - if (!isNumberMatrix(_matrix1) || !isNumberMatrix(_matrix2)) { - throw new Error(_t("The arguments matrix1 and matrix2 must be matrices of numbers.")); - } - return multiplyMatrices(_matrix1, _matrix2); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SUMPRODUCT - // ----------------------------------------------------------------------------- - const SUMPRODUCT = { - description: _t("Calculates the sum of the products of corresponding entries in equal-sized ranges."), - args: [ - arg("range1 (number, range)", _t("The first range whose entries will be multiplied with corresponding entries in the other ranges.")), - arg("range2 (number, range, repeating)", _t("The other range whose entries will be multiplied with corresponding entries in the other ranges.")), - ], - returns: ["NUMBER"], - compute: function (...args) { - assertSameDimensions(_t("All the ranges must have the same dimensions."), ...args); - const _args = args.map(toMatrix); - let result = 0; - for (let col = 0; col < _args[0].length; col++) { - for (let row = 0; row < _args[0][col].length; row++) { - if (!_args.every((range) => typeof range[col][row] === "number")) { - continue; - } - let product = 1; - for (const range of _args) { - product *= toNumber(range[col][row], this.locale); - } - result += product; - } - } - return result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SUMX2MY2 - // ----------------------------------------------------------------------------- - /** - * Return the sum of the callback applied to each pair of values in the two arrays. - * - * Ignore the pairs X,Y where one of the value isn't a number. Throw an error if no pair of numbers is found. - */ - function getSumXAndY(arrayX, arrayY, cb) { - assertSameDimensions("The arguments array_x and array_y must have the same dimensions.", arrayX, arrayY); - const _arrayX = toMatrix(arrayX); - const _arrayY = toMatrix(arrayY); - let validPairFound = false; - let result = 0; - for (const col in _arrayX) { - for (const row in _arrayX[col]) { - const arrayXValue = _arrayX[col][row]; - const arrayYValue = _arrayY[col][row]; - if (typeof arrayXValue !== "number" || typeof arrayYValue !== "number") { - continue; - } - validPairFound = true; - result += cb(arrayXValue, arrayYValue); - } - } - if (!validPairFound) { - throw new Error("The arguments array_x and array_y must contain at least one pair of numbers."); - } - return result; - } - const SUMX2MY2 = { - description: _t("Calculates the sum of the difference of the squares of the values in two array."), - args: [ - arg("array_x (number, range)", _t("The array or range of values whose squares will be reduced by the squares of corresponding entries in array_y and added together.")), - arg("array_y (number, range)", _t("The array or range of values whose squares will be subtracted from the squares of corresponding entries in array_x and added together.")), - ], - returns: ["NUMBER"], - compute: function (arrayX, arrayY) { - return getSumXAndY(arrayX, arrayY, (x, y) => x ** 2 - y ** 2); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SUMX2PY2 - // ----------------------------------------------------------------------------- - const SUMX2PY2 = { - description: _t("Calculates the sum of the sum of the squares of the values in two array."), - args: [ - arg("array_x (number, range)", _t("The array or range of values whose squares will be added to the squares of corresponding entries in array_y and added together.")), - arg("array_y (number, range)", _t("The array or range of values whose squares will be added to the squares of corresponding entries in array_x and added together.")), - ], - returns: ["NUMBER"], - compute: function (arrayX, arrayY) { - return getSumXAndY(arrayX, arrayY, (x, y) => x ** 2 + y ** 2); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SUMXMY2 - // ----------------------------------------------------------------------------- - const SUMXMY2 = { - description: _t("Calculates the sum of squares of the differences of values in two array."), - args: [ - arg("array_x (number, range)", _t("The array or range of values that will be reduced by corresponding entries in array_y, squared, and added together.")), - arg("array_y (number, range)", _t("The array or range of values that will be subtracted from corresponding entries in array_x, the result squared, and all such results added together.")), - ], - returns: ["NUMBER"], - compute: function (arrayX, arrayY) { - return getSumXAndY(arrayX, arrayY, (x, y) => (x - y) ** 2); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TOCOL - // ----------------------------------------------------------------------------- - const TO_COL_ROW_DEFAULT_IGNORE = 0; - const TO_COL_ROW_DEFAULT_SCAN = false; - const TO_COL_ROW_ARGS = [ - arg("array (any, range)", _t("The array which will be transformed.")), - arg(`ignore (number, default=${TO_COL_ROW_DEFAULT_IGNORE})`, _t("The control to ignore blanks and errors. 0 (default) is to keep all values, 1 is to ignore blanks, 2 is to ignore errors, and 3 is to ignore blanks and errors.")), - arg(`scan_by_column (number, default=${TO_COL_ROW_DEFAULT_SCAN})`, _t("Whether the array should be scanned by column. True scans the array by column and false (default) \ - scans the array by row.")), - ]; - const TOCOL = { - description: _t("Transforms a range of cells into a single column."), - args: TO_COL_ROW_ARGS, - returns: ["RANGE"], - computeValueAndFormat: function (array, ignore = { value: TO_COL_ROW_DEFAULT_IGNORE }, scanByColumn = { value: TO_COL_ROW_DEFAULT_SCAN }) { - const _array = toMatrix(array); - const _ignore = toInteger(ignore.value, this.locale); - const _scanByColumn = toBoolean(scanByColumn.value); - assert(() => _ignore >= 0 && _ignore <= 3, _t("Argument ignore must be between 0 and 3")); - // TODO : implement ignore value 2 (ignore error) & 3 (ignore blanks and errors) once we can have errors in - // the array w/o crashing - const result = (_scanByColumn ? _array : transposeMatrix(_array)) - .flat() - .filter((item) => (_ignore !== 1 && _ignore !== 3) || (item.value !== undefined && item.value !== null)); - if (result.length === 0) { - throw new NotAvailableError(_t("No results for the given arguments of TOCOL.")); - } - return [result]; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TOROW - // ----------------------------------------------------------------------------- - const TOROW = { - description: _t("Transforms a range of cells into a single row."), - args: TO_COL_ROW_ARGS, - returns: ["RANGE"], - computeValueAndFormat: function (array, ignore = { value: TO_COL_ROW_DEFAULT_IGNORE }, scanByColumn = { value: TO_COL_ROW_DEFAULT_SCAN }) { - const _array = toMatrix(array); - const _ignore = toInteger(ignore.value, this.locale); - const _scanByColumn = toBoolean(scanByColumn.value); - assert(() => _ignore >= 0 && _ignore <= 3, _t("Argument ignore must be between 0 and 3")); - // TODO : implement ignore value 2 (ignore error) & 3 (ignore blanks and errors) once we can have errors in - // the array w/o crashing - const result = (_scanByColumn ? _array : transposeMatrix(_array)) - .flat() - .filter((item) => (_ignore !== 1 && _ignore !== 3) || (item.value !== undefined && item.value !== null)) - .map((item) => [item]); - if (result.length === 0 || result[0].length === 0) { - throw new NotAvailableError(_t("No results for the given arguments of TOROW.")); - } - return result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TRANSPOSE - // ----------------------------------------------------------------------------- - const TRANSPOSE = { - description: _t("Transposes the rows and columns of a range."), - args: [arg("range (any, range)", _t("The range to be transposed."))], - returns: ["RANGE"], - computeValueAndFormat: function (arg) { - const _array = toMatrix(arg); - const nbColumns = _array[0].length; - const nbRows = _array.length; - return generateMatrix(nbColumns, nbRows, (col, row) => _array[row][col]); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // VSTACK - // ----------------------------------------------------------------------------- - const VSTACK = { - description: _t("Appends ranges vertically and in sequence to return a larger array."), - args: [ - arg("range1 (any, range)", _t("The first range to be appended.")), - arg("range2 (any, range, repeating)", _t("Additional ranges to add to range1.")), - ], - returns: ["RANGE"], - computeValueAndFormat: function (...ranges) { - const nbColumns = Math.max(...ranges.map((range) => toMatrix(range).length)); - const nbRows = ranges.reduce((acc, range) => acc + toMatrix(range)[0].length, 0); - const result = Array(nbColumns) - .fill([]) - .map(() => Array(nbRows).fill({ value: 0 })); // TODO fill with #N/A - let currentRow = 0; - for (const range of ranges) { - const _array = toMatrix(range); - for (let col = 0; col < _array.length; col++) { - for (let row = 0; row < _array[col].length; row++) { - result[col][currentRow + row] = _array[col][row]; - } - } - currentRow += _array[0].length; - } - return result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // WRAPCOLS - // ----------------------------------------------------------------------------- - const WRAPCOLS = { - description: _t("Wraps the provided row or column of cells by columns after a specified number of elements to form a new array."), - args: [ - arg("range (any, range)", _t("The range to wrap.")), - arg("wrap_count (number)", _t("The maximum number of cells for each column, rounded down to the nearest whole number.")), - arg("pad_with (any, default=0)", // TODO : replace with #N/A - _t("The value with which to fill the extra cells in the range.")), - ], - returns: ["RANGE"], - computeValueAndFormat: function (range, wrapCount, padWith = { value: 0 }) { - const _array = toMatrix(range); - const nbRows = toInteger(wrapCount?.value, this.locale); - assertSingleColOrRow(_t("Argument range must be a single row or column."), _array); - const array = _array.flat(); - const nbColumns = Math.ceil(array.length / nbRows); - return generateMatrix(nbColumns, nbRows, (col, row) => { - const index = col * nbRows + row; - return index < array.length ? array[index] : padWith; - }); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // WRAPROWS - // ----------------------------------------------------------------------------- - const WRAPROWS = { - description: _t("Wraps the provided row or column of cells by rows after a specified number of elements to form a new array."), - args: [ - arg("range (any, range)", _t("The range to wrap.")), - arg("wrap_count (number)", _t("The maximum number of cells for each row, rounded down to the nearest whole number.")), - arg("pad_with (any, default=0)", // TODO : replace with #N/A - _t("The value with which to fill the extra cells in the range.")), - ], - returns: ["RANGE"], - computeValueAndFormat: function (range, wrapCount, padWith = { value: 0 }) { - const _array = toMatrix(range); - const nbColumns = toInteger(wrapCount?.value, this.locale); - assertSingleColOrRow(_t("Argument range must be a single row or column."), _array); - const array = _array.flat(); - const nbRows = Math.ceil(array.length / nbColumns); - return generateMatrix(nbColumns, nbRows, (col, row) => { - const index = row * nbColumns + col; - return index < array.length ? array[index] : padWith; - }); - }, - isExported: true, - }; - - var array = /*#__PURE__*/Object.freeze({ - __proto__: null, - ARRAY_CONSTRAIN: ARRAY_CONSTRAIN, - CHOOSECOLS: CHOOSECOLS, - CHOOSEROWS: CHOOSEROWS, - EXPAND: EXPAND, - FLATTEN: FLATTEN, - FREQUENCY: FREQUENCY, - HSTACK: HSTACK, - MDETERM: MDETERM, - MINVERSE: MINVERSE, - MMULT: MMULT, - SUMPRODUCT: SUMPRODUCT, - SUMX2MY2: SUMX2MY2, - SUMX2PY2: SUMX2PY2, - SUMXMY2: SUMXMY2, - TOCOL: TOCOL, - TOROW: TOROW, - TRANSPOSE: TRANSPOSE, - VSTACK: VSTACK, - WRAPCOLS: WRAPCOLS, - WRAPROWS: WRAPROWS - }); - - // ----------------------------------------------------------------------------- - // FORMAT.LARGE.NUMBER - // ----------------------------------------------------------------------------- - const FORMAT_LARGE_NUMBER = { - description: _t(`Apply a large number format`), - args: [ - arg("value (number)", _t("The number.")), - arg("unit (string, optional)", _t("The formatting unit. Use 'k', 'm', or 'b' to force the unit")), - ], - returns: ["NUMBER"], - computeFormat: function (arg, unit) { - const value = Math.abs(toNumber(arg?.value, this.locale)); - const format = arg?.format; - if (unit !== undefined) { - const postFix = unit?.value; - switch (postFix) { - case "k": - return createLargeNumberFormat(format, 1e3, "k", this.locale); - case "m": - return createLargeNumberFormat(format, 1e6, "m", this.locale); - case "b": - return createLargeNumberFormat(format, 1e9, "b", this.locale); - default: - throw new Error(_t("The formatting unit should be 'k', 'm' or 'b'.")); - } - } - if (value < 1e5) { - return createLargeNumberFormat(format, 0, "", this.locale); - } - else if (value < 1e8) { - return createLargeNumberFormat(format, 1e3, "k", this.locale); - } - else if (value < 1e11) { - return createLargeNumberFormat(format, 1e6, "m", this.locale); - } - return createLargeNumberFormat(format, 1e9, "b", this.locale); - }, - compute: function (value) { - return toNumber(value, this.locale); - }, - }; - - var misc$1 = /*#__PURE__*/Object.freeze({ - __proto__: null, - FORMAT_LARGE_NUMBER: FORMAT_LARGE_NUMBER - }); - - const DEFAULT_FACTOR = 1; - const DEFAULT_MODE = 0; - const DEFAULT_PLACES = 0; - const DEFAULT_SIGNIFICANCE = 1; - const DECIMAL_REPRESENTATION = /^-?[a-z0-9]+$/i; - // ----------------------------------------------------------------------------- - // ABS - // ----------------------------------------------------------------------------- - const ABS = { - description: _t("Absolute value of a number."), - args: [arg("value (number)", _t("The number of which to return the absolute value."))], - returns: ["NUMBER"], - compute: function (value) { - return Math.abs(toNumber(value, this.locale)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ACOS - // ----------------------------------------------------------------------------- - const ACOS = { - description: _t("Inverse cosine of a value, in radians."), - args: [ - arg("value (number)", _t("The value for which to calculate the inverse cosine. Must be between -1 and 1, inclusive.")), - ], - returns: ["NUMBER"], - compute: function (value) { - const _value = toNumber(value, this.locale); - assert(() => Math.abs(_value) <= 1, _t("The value (%s) must be between -1 and 1 inclusive.", _value.toString())); - return Math.acos(_value); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ACOSH - // ----------------------------------------------------------------------------- - const ACOSH = { - description: _t("Inverse hyperbolic cosine of a number."), - args: [ - arg("value (number)", _t("The value for which to calculate the inverse hyperbolic cosine. Must be greater than or equal to 1.")), - ], - returns: ["NUMBER"], - compute: function (value) { - const _value = toNumber(value, this.locale); - assert(() => _value >= 1, _t("The value (%s) must be greater than or equal to 1.", _value.toString())); - return Math.acosh(_value); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ACOT - // ----------------------------------------------------------------------------- - const ACOT = { - description: _t("Inverse cotangent of a value."), - args: [arg("value (number)", _t("The value for which to calculate the inverse cotangent."))], - returns: ["NUMBER"], - compute: function (value) { - const _value = toNumber(value, this.locale); - const sign = Math.sign(_value) || 1; - // ACOT has two possible configurations: - // @compatibility Excel: return Math.PI / 2 - Math.atan(toNumber(_value, this.locale)); - // @compatibility Google: return sign * Math.PI / 2 - Math.atan(toNumber(_value, this.locale)); - return (sign * Math.PI) / 2 - Math.atan(_value); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ACOTH - // ----------------------------------------------------------------------------- - const ACOTH = { - description: _t("Inverse hyperbolic cotangent of a value."), - args: [ - arg("value (number)", _t("The value for which to calculate the inverse hyperbolic cotangent. Must not be between -1 and 1, inclusive.")), - ], - returns: ["NUMBER"], - compute: function (value) { - const _value = toNumber(value, this.locale); - assert(() => Math.abs(_value) > 1, _t("The value (%s) cannot be between -1 and 1 inclusive.", _value.toString())); - return Math.log((_value + 1) / (_value - 1)) / 2; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ASIN - // ----------------------------------------------------------------------------- - const ASIN = { - description: _t("Inverse sine of a value, in radians."), - args: [ - arg("value (number)", _t("The value for which to calculate the inverse sine. Must be between -1 and 1, inclusive.")), - ], - returns: ["NUMBER"], - compute: function (value) { - const _value = toNumber(value, this.locale); - assert(() => Math.abs(_value) <= 1, _t("The value (%s) must be between -1 and 1 inclusive.", _value.toString())); - return Math.asin(_value); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ASINH - // ----------------------------------------------------------------------------- - const ASINH = { - description: _t("Inverse hyperbolic sine of a number."), - args: [ - arg("value (number)", _t("The value for which to calculate the inverse hyperbolic sine.")), - ], - returns: ["NUMBER"], - compute: function (value) { - return Math.asinh(toNumber(value, this.locale)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ATAN - // ----------------------------------------------------------------------------- - const ATAN = { - description: _t("Inverse tangent of a value, in radians."), - args: [arg("value (number)", _t("The value for which to calculate the inverse tangent."))], - returns: ["NUMBER"], - compute: function (value) { - return Math.atan(toNumber(value, this.locale)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ATAN2 - // ----------------------------------------------------------------------------- - const ATAN2 = { - description: _t("Angle from the X axis to a point (x,y), in radians."), - args: [ - arg("x (number)", _t("The x coordinate of the endpoint of the line segment for which to calculate the angle from the x-axis.")), - arg("y (number)", _t("The y coordinate of the endpoint of the line segment for which to calculate the angle from the x-axis.")), - ], - returns: ["NUMBER"], - compute: function (x, y) { - const _x = toNumber(x, this.locale); - const _y = toNumber(y, this.locale); - assert(() => _x !== 0 || _y !== 0, _t(`Function [[FUNCTION_NAME]] caused a divide by zero error.`)); - return Math.atan2(_y, _x); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ATANH - // ----------------------------------------------------------------------------- - const ATANH = { - description: _t("Inverse hyperbolic tangent of a number."), - args: [ - arg("value (number)", _t("The value for which to calculate the inverse hyperbolic tangent. Must be between -1 and 1, exclusive.")), - ], - returns: ["NUMBER"], - compute: function (value) { - const _value = toNumber(value, this.locale); - assert(() => Math.abs(_value) < 1, _t("The value (%s) must be between -1 and 1 exclusive.", _value.toString())); - return Math.atanh(_value); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // CEILING - // ----------------------------------------------------------------------------- - const CEILING = { - description: _t(`Rounds number up to nearest multiple of factor.`), - args: [ - arg("value (number)", _t("The value to round up to the nearest integer multiple of factor.")), - arg(`factor (number, default=${DEFAULT_FACTOR})`, _t("The number to whose multiples value will be rounded.")), - ], - returns: ["NUMBER"], - computeFormat: (value) => value?.format, - compute: function (value, factor = DEFAULT_FACTOR) { - const _value = toNumber(value, this.locale); - const _factor = toNumber(factor, this.locale); - assert(() => _factor >= 0 || _value <= 0, _t("The factor (%s) must be positive when the value (%s) is positive.", _factor.toString(), _value.toString())); - return _factor ? Math.ceil(_value / _factor) * _factor : 0; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // CEILING.MATH - // ----------------------------------------------------------------------------- - const CEILING_MATH = { - description: _t(`Rounds number up to nearest multiple of factor.`), - args: [ - arg("number (number)", _t("The value to round up to the nearest integer multiple of significance.")), - arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded. The sign of significance will be ignored.")), - arg(`mode (number, default=${DEFAULT_MODE})`, _t("If number is negative, specifies the rounding direction. If 0 or blank, it is rounded towards zero. Otherwise, it is rounded away from zero.")), - ], - returns: ["NUMBER"], - computeFormat: (number) => number?.format, - compute: function (number, significance = DEFAULT_SIGNIFICANCE, mode = DEFAULT_MODE) { - let _significance = toNumber(significance, this.locale); - if (_significance === 0) { - return 0; - } - const _number = toNumber(number, this.locale); - _significance = Math.abs(_significance); - if (_number >= 0) { - return Math.ceil(_number / _significance) * _significance; - } - const _mode = toNumber(mode, this.locale); - if (_mode === 0) { - return -Math.floor(Math.abs(_number) / _significance) * _significance; - } - return -Math.ceil(Math.abs(_number) / _significance) * _significance; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // CEILING.PRECISE - // ----------------------------------------------------------------------------- - const CEILING_PRECISE = { - description: _t(`Rounds number up to nearest multiple of factor.`), - args: [ - arg("number (number)", _t("The value to round up to the nearest integer multiple of significance.")), - arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded.")), - ], - returns: ["NUMBER"], - computeFormat: (number) => number?.format, - compute: function (number, significance) { - return CEILING_MATH.compute.bind(this)(number, significance, 0); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COS - // ----------------------------------------------------------------------------- - const COS = { - description: _t("Cosine of an angle provided in radians."), - args: [arg("angle (number)", _t("The angle to find the cosine of, in radians."))], - returns: ["NUMBER"], - compute: function (angle) { - return Math.cos(toNumber(angle, this.locale)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COSH - // ----------------------------------------------------------------------------- - const COSH = { - description: _t("Hyperbolic cosine of any real number."), - args: [arg("value (number)", _t("Any real value to calculate the hyperbolic cosine of."))], - returns: ["NUMBER"], - compute: function (value) { - return Math.cosh(toNumber(value, this.locale)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COT - // ----------------------------------------------------------------------------- - const COT = { - description: _t("Cotangent of an angle provided in radians."), - args: [arg("angle (number)", _t("The angle to find the cotangent of, in radians."))], - returns: ["NUMBER"], - compute: function (angle) { - const _angle = toNumber(angle, this.locale); - assert(() => _angle !== 0, _t(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`)); - return 1 / Math.tan(_angle); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COTH - // ----------------------------------------------------------------------------- - const COTH = { - description: _t("Hyperbolic cotangent of any real number."), - args: [arg("value (number)", _t("Any real value to calculate the hyperbolic cotangent of."))], - returns: ["NUMBER"], - compute: function (value) { - const _value = toNumber(value, this.locale); - assert(() => _value !== 0, _t(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`)); - return 1 / Math.tanh(_value); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COUNTBLANK - // ----------------------------------------------------------------------------- - const COUNTBLANK = { - description: _t("Number of empty values."), - args: [ - arg("value1 (any, range)", _t("The first value or range in which to count the number of blanks.")), - arg("value2 (any, range, repeating)", _t("Additional values or ranges in which to count the number of blanks.")), - ], - returns: ["NUMBER"], - compute: function (...argsValues) { - return reduceAny(argsValues, (acc, a) => (a === null || a === undefined || a === "" ? acc + 1 : acc), 0); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COUNTIF - // ----------------------------------------------------------------------------- - const COUNTIF = { - description: _t("A conditional count across a range."), - args: [ - arg("range (range)", _t("The range that is tested against criterion.")), - arg("criterion (string)", _t("The pattern or test to apply to range.")), - ], - returns: ["NUMBER"], - compute: function (...argsValues) { - let count = 0; - visitMatchingRanges(argsValues, (i, j) => { - count += 1; - }, this.locale); - return count; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COUNTIFS - // ----------------------------------------------------------------------------- - const COUNTIFS = { - description: _t("Count values depending on multiple criteria."), - args: [ - arg("criteria_range1 (range)", _t("The range to check against criterion1.")), - arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1.")), - arg("criteria_range2 (any, range, repeating)", _t("Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.")), - arg("criterion2 (string, repeating)", _t("Additional criteria to check.")), - ], - returns: ["NUMBER"], - compute: function (...argsValues) { - let count = 0; - visitMatchingRanges(argsValues, (i, j) => { - count += 1; - }, this.locale); - return count; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COUNTUNIQUE - // ----------------------------------------------------------------------------- - function isDefined(value) { - switch (value) { - case undefined: - return false; - case "": - return false; - case null: - return false; - default: - return true; - } - } - const COUNTUNIQUE = { - description: _t("Counts number of unique values in a range."), - args: [ - arg("value1 (any, range)", _t("The first value or range to consider for uniqueness.")), - arg("value2 (any, range, repeating)", _t("Additional values or ranges to consider for uniqueness.")), - ], - returns: ["NUMBER"], - compute: function (...argsValues) { - return reduceAny(argsValues, (acc, a) => (isDefined(a) ? acc.add(a) : acc), new Set()).size; - }, - }; - // ----------------------------------------------------------------------------- - // COUNTUNIQUEIFS - // ----------------------------------------------------------------------------- - const COUNTUNIQUEIFS = { - description: _t("Counts number of unique values in a range, filtered by a set of criteria."), - args: [ - arg("range (range)", _t("The range of cells from which the number of unique values will be counted.")), - arg("criteria_range1 (range)", _t("The range of cells over which to evaluate criterion1.")), - arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.")), - arg("criteria_range2 (any, range, repeating)", _t("Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.")), - arg("criterion2 (string, repeating)", _t("The pattern or test to apply to criteria_range2.")), - ], - returns: ["NUMBER"], - compute: function (range, ...argsValues) { - let uniqueValues = new Set(); - visitMatchingRanges(argsValues, (i, j) => { - const value = range[i][j]; - if (isDefined(value)) { - uniqueValues.add(value); - } - }, this.locale); - return uniqueValues.size; - }, - }; - // ----------------------------------------------------------------------------- - // CSC - // ----------------------------------------------------------------------------- - const CSC = { - description: _t("Cosecant of an angle provided in radians."), - args: [arg("angle (number)", _t("The angle to find the cosecant of, in radians."))], - returns: ["NUMBER"], - compute: function (angle) { - const _angle = toNumber(angle, this.locale); - assert(() => _angle !== 0, _t(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`)); - return 1 / Math.sin(_angle); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // CSCH - // ----------------------------------------------------------------------------- - const CSCH = { - description: _t("Hyperbolic cosecant of any real number."), - args: [arg("value (number)", _t("Any real value to calculate the hyperbolic cosecant of."))], - returns: ["NUMBER"], - compute: function (value) { - const _value = toNumber(value, this.locale); - assert(() => _value !== 0, _t(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`)); - return 1 / Math.sinh(_value); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DECIMAL - // ----------------------------------------------------------------------------- - const DECIMAL = { - description: _t("Converts from another base to decimal."), - args: [ - arg("value (string)", _t("The number to convert.")), - arg(",base (number)", _t("The base to convert the value from.")), - ], - returns: ["NUMBER"], - compute: function (value, base) { - let _base = toNumber(base, this.locale); - _base = Math.floor(_base); - assert(() => 2 <= _base && _base <= 36, _t("The base (%s) must be between 2 and 36 inclusive.", _base.toString())); - const _value = toString(value); - if (_value === "") { - return 0; - } - /** - * @compatibility: on Google sheets, expects the parameter 'value' to be positive. - * Return error if 'value' is positive. - * Remove '-?' in the next regex to catch this error. - */ - assert(() => !!DECIMAL_REPRESENTATION.test(_value), _t("The value (%s) must be a valid base %s representation.", _value, _base.toString())); - const deci = parseInt(_value, _base); - assert(() => !isNaN(deci), _t("The value (%s) must be a valid base %s representation.", _value, _base.toString())); - return deci; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DEGREES - // ----------------------------------------------------------------------------- - const DEGREES = { - description: _t(`Converts an angle value in radians to degrees.`), - args: [arg("angle (number)", _t("The angle to convert from radians to degrees."))], - returns: ["NUMBER"], - compute: function (angle) { - return (toNumber(angle, this.locale) * 180) / Math.PI; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // EXP - // ----------------------------------------------------------------------------- - const EXP = { - description: _t(`Euler's number, e (~2.718) raised to a power.`), - args: [arg("value (number)", _t("The exponent to raise e."))], - returns: ["NUMBER"], - compute: function (value) { - return Math.exp(toNumber(value, this.locale)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // FLOOR - // ----------------------------------------------------------------------------- - const FLOOR = { - description: _t(`Rounds number down to nearest multiple of factor.`), - args: [ - arg("value (number)", _t("The value to round down to the nearest integer multiple of factor.")), - arg(`factor (number, default=${DEFAULT_FACTOR})`, _t("The number to whose multiples value will be rounded.")), - ], - returns: ["NUMBER"], - computeFormat: (value) => value?.format, - compute: function (value, factor = DEFAULT_FACTOR) { - const _value = toNumber(value, this.locale); - const _factor = toNumber(factor, this.locale); - assert(() => _factor >= 0 || _value <= 0, _t("The factor (%s) must be positive when the value (%s) is positive.", _factor.toString(), _value.toString())); - return _factor ? Math.floor(_value / _factor) * _factor : 0; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // FLOOR.MATH - // ----------------------------------------------------------------------------- - const FLOOR_MATH = { - description: _t(`Rounds number down to nearest multiple of factor.`), - args: [ - arg("number (number)", _t("The value to round down to the nearest integer multiple of significance.")), - arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded. The sign of significance will be ignored.")), - arg(`mode (number, default=${DEFAULT_MODE})`, _t("If number is negative, specifies the rounding direction. If 0 or blank, it is rounded away from zero. Otherwise, it is rounded towards zero.")), - ], - returns: ["NUMBER"], - computeFormat: (number) => number?.format, - compute: function (number, significance = DEFAULT_SIGNIFICANCE, mode = DEFAULT_MODE) { - let _significance = toNumber(significance, this.locale); - if (_significance === 0) { - return 0; - } - const _number = toNumber(number, this.locale); - _significance = Math.abs(_significance); - if (_number >= 0) { - return Math.floor(_number / _significance) * _significance; - } - const _mode = toNumber(mode, this.locale); - if (_mode === 0) { - return -Math.ceil(Math.abs(_number) / _significance) * _significance; - } - return -Math.floor(Math.abs(_number) / _significance) * _significance; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // FLOOR.PRECISE - // ----------------------------------------------------------------------------- - const FLOOR_PRECISE = { - description: _t(`Rounds number down to nearest multiple of factor.`), - args: [ - arg("number (number)", _t("The value to round down to the nearest integer multiple of significance.")), - arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded.")), - ], - returns: ["NUMBER"], - computeFormat: (number) => number?.format, - compute: function (number, significance = DEFAULT_SIGNIFICANCE) { - return FLOOR_MATH.compute.bind(this)(number, significance, 0); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ISEVEN - // ----------------------------------------------------------------------------- - const ISEVEN = { - description: _t(`Whether the provided value is even.`), - args: [arg("value (number)", _t("The value to be verified as even."))], - returns: ["BOOLEAN"], - compute: function (value) { - const _value = strictToNumber(value, this.locale); - return Math.floor(Math.abs(_value)) & 1 ? false : true; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ISO.CEILING - // ----------------------------------------------------------------------------- - const ISO_CEILING = { - description: _t(`Rounds number up to nearest multiple of factor.`), - args: [ - arg("number (number)", _t("The value to round up to the nearest integer multiple of significance.")), - arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded.")), - ], - returns: ["NUMBER"], - computeFormat: (number) => number?.format, - compute: function (number, significance = DEFAULT_SIGNIFICANCE) { - return CEILING_MATH.compute.bind(this)(number, significance, 0); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ISODD - // ----------------------------------------------------------------------------- - const ISODD = { - description: _t(`Whether the provided value is even.`), - args: [arg("value (number)", _t("The value to be verified as even."))], - returns: ["BOOLEAN"], - compute: function (value) { - const _value = strictToNumber(value, this.locale); - return Math.floor(Math.abs(_value)) & 1 ? true : false; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // LN - // ----------------------------------------------------------------------------- - const LN = { - description: _t(`The logarithm of a number, base e (euler's number).`), - args: [arg("value (number)", _t("The value for which to calculate the logarithm, base e."))], - returns: ["NUMBER"], - compute: function (value) { - const _value = toNumber(value, this.locale); - assert(() => _value > 0, _t("The value (%s) must be strictly positive.", _value.toString())); - return Math.log(_value); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MOD - // ----------------------------------------------------------------------------- - const MOD = { - description: _t(`Modulo (remainder) operator.`), - args: [ - arg("dividend (number)", _t("The number to be divided to find the remainder.")), - arg("divisor (number)", _t("The number to divide by.")), - ], - returns: ["NUMBER"], - computeFormat: (dividend) => dividend?.format, - compute: function (dividend, divisor) { - const _divisor = toNumber(divisor, this.locale); - assert(() => _divisor !== 0, _t("The divisor must be different from 0.")); - const _dividend = toNumber(dividend, this.locale); - const modulus = _dividend % _divisor; - // -42 % 10 = -2 but we want 8, so need the code below - if ((modulus > 0 && _divisor < 0) || (modulus < 0 && _divisor > 0)) { - return modulus + _divisor; - } - return modulus; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MUNIT - // ----------------------------------------------------------------------------- - const MUNIT = { - description: _t("Returns a n x n unit matrix, where n is the input dimension."), - args: [ - arg("dimension (number)", _t("An integer specifying the dimension size of the unit matrix. It must be positive.")), - ], - returns: ["RANGE"], - compute: function (n) { - const _n = toInteger(n, this.locale); - assertPositive(_t("The argument dimension must be positive"), _n); - return getUnitMatrix(_n); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ODD - // ----------------------------------------------------------------------------- - const ODD = { - description: _t(`Rounds a number up to the nearest odd integer.`), - args: [arg("value (number)", _t("The value to round to the next greatest odd number."))], - returns: ["NUMBER"], - computeFormat: (number) => number?.format, - compute: function (value) { - const _value = toNumber(value, this.locale); - let temp = Math.ceil(Math.abs(_value)); - temp = temp & 1 ? temp : temp + 1; - return _value < 0 ? -temp : temp; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // PI - // ----------------------------------------------------------------------------- - const PI = { - description: _t(`The number pi.`), - args: [], - returns: ["NUMBER"], - compute: function () { - return Math.PI; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // POWER - // ----------------------------------------------------------------------------- - const POWER = { - description: _t(`A number raised to a power.`), - args: [ - arg("base (number)", _t("The number to raise to the exponent power.")), - arg("exponent (number)", _t("The exponent to raise base to.")), - ], - returns: ["NUMBER"], - computeFormat: (base) => base?.format, - compute: function (base, exponent) { - const _base = toNumber(base, this.locale); - const _exponent = toNumber(exponent, this.locale); - assert(() => _base >= 0 || Number.isInteger(_exponent), _t("The exponent (%s) must be an integer when the base is negative.", _exponent.toString())); - return Math.pow(_base, _exponent); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // PRODUCT - // ----------------------------------------------------------------------------- - const PRODUCT = { - description: _t("Result of multiplying a series of numbers together."), - args: [ - arg("factor1 (number, range)", _t("The first number or range to calculate for the product.")), - arg("factor2 (number, range, repeating)", _t("More numbers or ranges to calculate for the product.")), - ], - returns: ["NUMBER"], - computeFormat: (factor1) => { - return isMatrix(factor1) ? factor1[0][0]?.format : factor1?.format; - }, - compute: function (...factors) { - let count = 0; - let acc = 1; - for (let n of factors) { - if (isMatrix(n)) { - for (let i of n) { - for (let j of i) { - if (typeof j === "number") { - acc *= j; - count += 1; - } - } - } - } - else if (n !== null && n !== undefined) { - acc *= strictToNumber(n, this.locale); - count += 1; - } - } - if (count === 0) { - return 0; - } - return acc; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // RAND - // ----------------------------------------------------------------------------- - const RAND = { - description: _t("A random number between 0 inclusive and 1 exclusive."), - args: [], - returns: ["NUMBER"], - compute: function () { - return Math.random(); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // RANDARRAY - // ----------------------------------------------------------------------------- - const RANDARRAY = { - description: _t("Returns a grid of random numbers between 0 inclusive and 1 exclusive."), - args: [ - arg("rows (number, default=1)", _t("The number of rows to be returned.")), - arg("columns (number, default=1)", _t("The number of columns to be returned.")), - arg("min (number, default=0)", _t("The minimum number you would like returned.")), - arg("max (number, default=1)", _t("The maximum number you would like returned.")), - arg("whole_number (number, default=FALSE)", _t("Return a whole number or a decimal value.")), - ], - returns: ["RANGE"], - compute: function (rows = 1, columns = 1, min = 0, max = 1, whole_number = false) { - const _cols = toInteger(columns, this.locale); - const _rows = toInteger(rows, this.locale); - const _min = toNumber(min, this.locale); - const _max = toNumber(max, this.locale); - const _whole_number = toBoolean(whole_number); - assertPositive(_t("The number columns (%s) must be positive.", _cols.toString()), _cols); - assertPositive(_t("The number rows (%s) must be positive.", _rows.toString()), _rows); - assert(() => _min <= _max, _t("The maximum (%s) must be greater than or equal to the minimum (%s).", _max.toString(), _min.toString())); - if (_whole_number) { - assert(() => Number.isInteger(_min) && Number.isInteger(_max), _t("The maximum (%s) and minimum (%s) must be integers when whole_number is TRUE.", _max.toString(), _min.toString())); - } - const result = Array(_cols); - for (let col = 0; col < _cols; col++) { - result[col] = Array(_rows); - for (let row = 0; row < _rows; row++) { - if (!_whole_number) { - result[col][row] = _min + Math.random() * (_max - _min); - } - else { - result[col][row] = Math.floor(Math.random() * (_max - _min + 1) + _min); - } - } - } - return result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // RANDBETWEEN - // ----------------------------------------------------------------------------- - const RANDBETWEEN = { - description: _t("Random integer between two values, inclusive."), - args: [ - arg("low (number)", _t("The low end of the random range.")), - arg("high (number)", _t("The high end of the random range.")), - ], - returns: ["NUMBER"], - computeFormat: (low) => low?.format, - compute: function (low, high) { - let _low = toNumber(low, this.locale); - if (!Number.isInteger(_low)) { - _low = Math.ceil(_low); - } - let _high = toNumber(high, this.locale); - if (!Number.isInteger(_high)) { - _high = Math.floor(_high); - } - assert(() => _low <= _high, _t("The high (%s) must be greater than or equal to the low (%s).", _high.toString(), _low.toString())); - return _low + Math.ceil((_high - _low + 1) * Math.random()) - 1; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ROUND - // ----------------------------------------------------------------------------- - const ROUND = { - description: _t("Rounds a number according to standard rules."), - args: [ - arg("value (number)", _t("The value to round to places number of places.")), - arg(`places (number, default=${DEFAULT_PLACES})`, _t("The number of decimal places to which to round.")), - ], - returns: ["NUMBER"], - computeFormat: (value) => value?.format, - compute: function (value, places = DEFAULT_PLACES) { - const _value = toNumber(value, this.locale); - let _places = toNumber(places, this.locale); - const absValue = Math.abs(_value); - let tempResult; - if (_places === 0) { - tempResult = Math.round(absValue); - } - else { - if (!Number.isInteger(_places)) { - _places = Math.trunc(_places); - } - tempResult = Math.round(absValue * Math.pow(10, _places)) / Math.pow(10, _places); - } - return _value >= 0 ? tempResult : -tempResult; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ROUNDDOWN - // ----------------------------------------------------------------------------- - const ROUNDDOWN = { - description: _t(`Rounds down a number.`), - args: [ - arg("value (number)", _t("The value to round to places number of places, always rounding down.")), - arg(`places (number, default=${DEFAULT_PLACES})`, _t("The number of decimal places to which to round.")), - ], - returns: ["NUMBER"], - computeFormat: (value) => value?.format, - compute: function (value, places = DEFAULT_PLACES) { - const _value = toNumber(value, this.locale); - let _places = toNumber(places, this.locale); - const absValue = Math.abs(_value); - let tempResult; - if (_places === 0) { - tempResult = Math.floor(absValue); - } - else { - if (!Number.isInteger(_places)) { - _places = Math.trunc(_places); - } - tempResult = Math.floor(absValue * Math.pow(10, _places)) / Math.pow(10, _places); - } - return _value >= 0 ? tempResult : -tempResult; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ROUNDUP - // ----------------------------------------------------------------------------- - const ROUNDUP = { - description: _t(`Rounds up a number.`), - args: [ - arg("value (number)", _t("The value to round to places number of places, always rounding up.")), - arg(`places (number, default=${DEFAULT_PLACES})`, _t("The number of decimal places to which to round.")), - ], - returns: ["NUMBER"], - computeFormat: (value) => value?.format, - compute: function (value, places = DEFAULT_PLACES) { - const _value = toNumber(value, this.locale); - let _places = toNumber(places, this.locale); - const absValue = Math.abs(_value); - let tempResult; - if (_places === 0) { - tempResult = Math.ceil(absValue); - } - else { - if (!Number.isInteger(_places)) { - _places = Math.trunc(_places); - } - tempResult = Math.ceil(absValue * Math.pow(10, _places)) / Math.pow(10, _places); - } - return _value >= 0 ? tempResult : -tempResult; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SEC - // ----------------------------------------------------------------------------- - const SEC = { - description: _t("Secant of an angle provided in radians."), - args: [arg("angle (number)", _t("The angle to find the secant of, in radians."))], - returns: ["NUMBER"], - compute: function (angle) { - return 1 / Math.cos(toNumber(angle, this.locale)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SECH - // ----------------------------------------------------------------------------- - const SECH = { - description: _t("Hyperbolic secant of any real number."), - args: [arg("value (number)", _t("Any real value to calculate the hyperbolic secant of."))], - returns: ["NUMBER"], - compute: function (value) { - return 1 / Math.cosh(toNumber(value, this.locale)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SIN - // ----------------------------------------------------------------------------- - const SIN = { - description: _t("Sine of an angle provided in radians."), - args: [arg("angle (number)", _t("The angle to find the sine of, in radians."))], - returns: ["NUMBER"], - compute: function (angle) { - return Math.sin(toNumber(angle, this.locale)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SINH - // ----------------------------------------------------------------------------- - const SINH = { - description: _t("Hyperbolic sine of any real number."), - args: [arg("value (number)", _t("Any real value to calculate the hyperbolic sine of."))], - returns: ["NUMBER"], - compute: function (value) { - return Math.sinh(toNumber(value, this.locale)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SQRT - // ----------------------------------------------------------------------------- - const SQRT = { - description: _t("Positive square root of a positive number."), - args: [arg("value (number)", _t("The number for which to calculate the positive square root."))], - returns: ["NUMBER"], - computeFormat: (value) => value?.format, - compute: function (value) { - const _value = toNumber(value, this.locale); - assert(() => _value >= 0, _t("The value (%s) must be positive or null.", _value.toString())); - return Math.sqrt(_value); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SUM - // ----------------------------------------------------------------------------- - const SUM = { - description: _t("Sum of a series of numbers and/or cells."), - args: [ - arg("value1 (number, range)", _t("The first number or range to add together.")), - arg("value2 (number, range, repeating)", _t("Additional numbers or ranges to add to value1.")), - ], - returns: ["NUMBER"], - computeFormat: (value1) => { - return isMatrix(value1) ? value1[0][0]?.format : value1?.format; - }, - compute: function (...values) { - return reduceNumbers(values, (acc, a) => acc + a, 0, this.locale); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SUMIF - // ----------------------------------------------------------------------------- - const SUMIF = { - description: _t("A conditional sum across a range."), - args: [ - arg("criteria_range (range)", _t("The range which is tested against criterion.")), - arg("criterion (string)", _t("The pattern or test to apply to range.")), - arg("sum_range (range, default=criteria_range)", _t("The range to be summed, if different from range.")), - ], - returns: ["NUMBER"], - compute: function (criteriaRange, criterion, sumRange) { - if (sumRange === undefined) { - sumRange = criteriaRange; - } - let sum = 0; - visitMatchingRanges([criteriaRange, criterion], (i, j) => { - const value = sumRange[i][j]; - if (typeof value === "number") { - sum += value; - } - }, this.locale); - return sum; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SUMIFS - // ----------------------------------------------------------------------------- - const SUMIFS = { - description: _t("Sums a range depending on multiple criteria."), - args: [ - arg("sum_range (range)", _t("The range to sum.")), - arg("criteria_range1 (range)", _t("The range to check against criterion1.")), - arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1.")), - arg("criteria_range2 (any, range, repeating)", _t("Additional ranges to check.")), - arg("criterion2 (string, repeating)", _t("Additional criteria to check.")), - ], - returns: ["NUMBER"], - compute: function (sumRange, ...criters) { - let sum = 0; - visitMatchingRanges(criters, (i, j) => { - const value = sumRange[i][j]; - if (typeof value === "number") { - sum += value; - } - }, this.locale); - return sum; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TAN - // ----------------------------------------------------------------------------- - const TAN = { - description: _t("Tangent of an angle provided in radians."), - args: [arg("angle (number)", _t("The angle to find the tangent of, in radians."))], - returns: ["NUMBER"], - compute: function (angle) { - return Math.tan(toNumber(angle, this.locale)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TANH - // ----------------------------------------------------------------------------- - const TANH = { - description: _t("Hyperbolic tangent of any real number."), - args: [arg("value (number)", _t("Any real value to calculate the hyperbolic tangent of."))], - returns: ["NUMBER"], - compute: function (value) { - return Math.tanh(toNumber(value, this.locale)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TRUNC - // ----------------------------------------------------------------------------- - const TRUNC = { - description: _t("Truncates a number."), - args: [ - arg("value (number)", _t("The value to be truncated.")), - arg(`places (number, default=${DEFAULT_PLACES})`, _t("The number of significant digits to the right of the decimal point to retain.")), - ], - returns: ["NUMBER"], - computeFormat: (value) => value?.format, - compute: function (value, places = DEFAULT_PLACES) { - const _value = toNumber(value, this.locale); - let _places = toNumber(places, this.locale); - if (_places === 0) { - return Math.trunc(_value); - } - if (!Number.isInteger(_places)) { - _places = Math.trunc(_places); - } - return Math.trunc(_value * Math.pow(10, _places)) / Math.pow(10, _places); - }, - isExported: true, - }; - - var math = /*#__PURE__*/Object.freeze({ - __proto__: null, - ABS: ABS, - ACOS: ACOS, - ACOSH: ACOSH, - ACOT: ACOT, - ACOTH: ACOTH, - ASIN: ASIN, - ASINH: ASINH, - ATAN: ATAN, - ATAN2: ATAN2, - ATANH: ATANH, - CEILING: CEILING, - CEILING_MATH: CEILING_MATH, - CEILING_PRECISE: CEILING_PRECISE, - COS: COS, - COSH: COSH, - COT: COT, - COTH: COTH, - COUNTBLANK: COUNTBLANK, - COUNTIF: COUNTIF, - COUNTIFS: COUNTIFS, - COUNTUNIQUE: COUNTUNIQUE, - COUNTUNIQUEIFS: COUNTUNIQUEIFS, - CSC: CSC, - CSCH: CSCH, - DECIMAL: DECIMAL, - DEGREES: DEGREES, - EXP: EXP, - FLOOR: FLOOR, - FLOOR_MATH: FLOOR_MATH, - FLOOR_PRECISE: FLOOR_PRECISE, - ISEVEN: ISEVEN, - ISODD: ISODD, - ISO_CEILING: ISO_CEILING, - LN: LN, - MOD: MOD, - MUNIT: MUNIT, - ODD: ODD, - PI: PI, - POWER: POWER, - PRODUCT: PRODUCT, - RAND: RAND, - RANDARRAY: RANDARRAY, - RANDBETWEEN: RANDBETWEEN, - ROUND: ROUND, - ROUNDDOWN: ROUNDDOWN, - ROUNDUP: ROUNDUP, - SEC: SEC, - SECH: SECH, - SIN: SIN, - SINH: SINH, - SQRT: SQRT, - SUM: SUM, - SUMIF: SUMIF, - SUMIFS: SUMIFS, - TAN: TAN, - TANH: TANH, - TRUNC: TRUNC - }); - - function assertSameNumberOfElements(...args) { - const dims = args[0].length; - args.forEach((arg, i) => assert(() => arg.length === dims, _t("[[FUNCTION_NAME]] has mismatched dimensions for argument %s (%s vs %s).", i.toString(), dims.toString(), arg.length.toString()))); - } - - function filterAndFlatData(dataY, dataX) { - const _flatDataY = []; - const _flatDataX = []; - let lenY = 0; - let lenX = 0; - visitAny([dataY], (y) => { - _flatDataY.push(y); - lenY += 1; - }); - visitAny([dataX], (x) => { - _flatDataX.push(x); - lenX += 1; - }); - assert(() => lenY === lenX, _t("[[FUNCTION_NAME]] has mismatched argument count %s vs %s.", lenY, lenX)); - const flatDataX = []; - const flatDataY = []; - for (let i = 0; i < lenY; i++) { - const valueY = _flatDataY[i]; - const valueX = _flatDataX[i]; - if (typeof valueY === "number" && typeof valueX === "number") { - flatDataY.push(valueY); - flatDataX.push(valueX); - } - } - return { flatDataX, flatDataY }; - } - // Note: dataY and dataX may not have the same dimension - function covariance(dataY, dataX, isSample) { - const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX); - const count = flatDataY.length; - assert(() => count !== 0 && (!isSample || count !== 1), _t(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`)); - let sumY = 0; - let sumX = 0; - for (let i = 0; i < count; i++) { - sumY += flatDataY[i]; - sumX += flatDataX[i]; - } - const averageY = sumY / count; - const averageX = sumX / count; - let acc = 0; - for (let i = 0; i < count; i++) { - acc += (flatDataY[i] - averageY) * (flatDataX[i] - averageX); - } - return acc / (count - (isSample ? 1 : 0)); - } - function variance(args, isSample, textAs0, locale) { - let count = 0; - let sum = 0; - const reduceFunction = textAs0 ? reduceNumbersTextAs0 : reduceNumbers; - sum = reduceFunction(args, (acc, a) => { - count += 1; - return acc + a; - }, 0, locale); - assert(() => count !== 0 && (!isSample || count !== 1), _t(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`)); - const average = sum / count; - return (reduceFunction(args, (acc, a) => acc + Math.pow(a - average, 2), 0, locale) / - (count - (isSample ? 1 : 0))); - } - function centile(data, percent, isInclusive, locale) { - const _percent = toNumber(percent, locale); - assert(() => (isInclusive ? 0 <= _percent && _percent <= 1 : 0 < _percent && _percent < 1), _t(`Function [[FUNCTION_NAME]] parameter 2 value is out of range.`)); - let sortedArray = []; - let index; - let count = 0; - visitAny(data, (d) => { - if (typeof d === "number") { - index = dichotomicSearch(sortedArray, d, "nextSmaller", "asc", sortedArray.length, (array, i) => array[i]); - sortedArray.splice(index + 1, 0, d); - count++; - } - }); - assert(() => count !== 0, _t(`[[FUNCTION_NAME]] has no valid input data.`)); - if (!isInclusive) { - // 2nd argument must be between 1/(n+1) and n/(n+1) with n the number of data - assert(() => 1 / (count + 1) <= _percent && _percent <= count / (count + 1), _t(`Function [[FUNCTION_NAME]] parameter 2 value is out of range.`)); - } - return percentile(sortedArray, _percent, isInclusive); - } - function prepareDataForRegression(X, Y, newX) { - const _X = X.length ? X : [range(1, Y.flat().length + 1)]; - const nVar = _X.length; - let _newX = newX.length ? newX : _X; - _newX = _newX.length === nVar ? transposeMatrix(_newX) : _newX; - return { _X, _newX }; - } - /* - * This function performs a linear regression on the data set. It returns an array with two elements. - * The first element is the slope, and the second element is the intercept. - * The linear regression line is: y = slope*x + intercept - * The function use the least squares method to find the best fit for the data set : - * see https://www.mathsisfun.com/data/least-squares-regression.html - * https://www.statology.org/standard-error-of-estimate/ - * https://agronomy4future.org/?p=16670 - * https://vitalflux.com/interpreting-f-statistics-in-linear-regression-formula-examples/ - * https://web.ist.utl.pt/~ist11038/compute/errtheory/,regression/regrthroughorigin.pdf - */ - function fullLinearRegression(X, Y, computeIntercept = true, verbose = false) { - const y = Y.flat(); - const n = y.length; - let { _X } = prepareDataForRegression(X, Y, []); - _X = _X.length === n ? transposeMatrix(_X) : _X.slice(); - assertSameNumberOfElements(_X[0], y); - const nVar = _X.length; - const nDeg = n - nVar - (computeIntercept ? 1 : 0); - const yMatrix = [y]; - const xMatrix = transposeMatrix(_X.reverse()); - let avgX = []; - for (let i = 0; i < nVar; i++) { - avgX.push(0); - if (computeIntercept) { - for (const xij of _X[i]) { - avgX[i] += xij; - } - avgX[i] /= n; - } - } - let avgY = 0; - if (computeIntercept) { - for (const yi of y) { - avgY += yi; - } - avgY /= n; - } - const redX = xMatrix.map((row) => row.map((value, i) => value - avgX[i])); - if (computeIntercept) { - xMatrix.forEach((row) => row.push(1)); - } - const coeffs = getLMSCoefficients(xMatrix, yMatrix); - if (!computeIntercept) { - coeffs.push([0]); - } - if (!verbose) { - return coeffs; - } - const dot1 = multiplyMatrices(redX, transposeMatrix(redX)); - const { inverted: dotInv } = invertMatrix(dot1); - if (dotInv === undefined) { - throw new Error(_t("Matrix is not invertible")); - } - let SSE = 0, SSR = 0; - for (let i = 0; i < n; i++) { - const yi = y[i] - avgY; - let temp = 0; - for (let j = 0; j < nVar; j++) { - const xi = redX[i][j]; - temp += xi * coeffs[j][0]; - } - const ei = yi - temp; - SSE += ei * ei; - SSR += temp * temp; - } - const RMSE = Math.sqrt(SSE / nDeg); - const r2 = SSR / (SSR + SSE); - const f_stat = SSR / nVar / (SSE / nDeg); - const deltaCoeffs = []; - for (let i = 0; i < nVar; i++) { - deltaCoeffs.push(RMSE * Math.sqrt(dotInv[i][i])); - } - if (computeIntercept) { - const dot2 = multiplyMatrices(dotInv, [avgX]); - const dot3 = multiplyMatrices(transposeMatrix([avgX]), dot2); - deltaCoeffs.push(RMSE * Math.sqrt(dot3[0][0] + 1 / y.length)); - } - const returned = [ - [coeffs[0][0], deltaCoeffs[0], r2, f_stat, SSR], - [coeffs[1][0], deltaCoeffs[1], RMSE, nDeg, SSE], - ]; - for (let i = 2; i < nVar; i++) { - returned.push([coeffs[i][0], deltaCoeffs[i], "", "", ""]); - } - if (computeIntercept) { - returned.push([coeffs[nVar][0], deltaCoeffs[nVar], "", "", ""]); - } - else { - returned.push([0, "", "", "", ""]); - } - return returned; - } - /* - This function performs a polynomial regression on the data set. It returns the coefficients of - the polynomial function that best fits the data set. - The polynomial function is: y = c0 + c1*x + c2*x^2 + ... + cn*x^n, where n is the order (degree) - of the polynomial. The returned coefficients are then in the form: [c0, c1, c2, ..., cn] - The function is based on the method of least squares : - see: https://mathworld.wolfram.com/LeastSquaresFittingPolynomial.html - */ - function polynomialRegression(flatY, flatX, order, intercept) { - assertSameNumberOfElements(flatX, flatY); - assert(() => order >= 1, _t(`Function [[FUNCTION_NAME]] A regression of order less than 1 cannot be possible.`)); - const yMatrix = [flatY]; - const xMatrix = flatX.map((x) => range(0, order).map((i) => Math.pow(x, order - i))); - if (intercept) { - xMatrix.forEach((row) => row.push(1)); - } - const coeffs = getLMSCoefficients(xMatrix, yMatrix); - if (!intercept) { - coeffs.push([0]); - } - return coeffs; - } - function getLMSCoefficients(xMatrix, yMatrix) { - const xMatrixT = transposeMatrix(xMatrix); - const dot1 = multiplyMatrices(xMatrix, xMatrixT); - const { inverted: dotInv } = invertMatrix(dot1); - if (dotInv === undefined) { - throw new Error(_t("Matrix is not invertible")); - } - const dot2 = multiplyMatrices(xMatrix, yMatrix); - return transposeMatrix(multiplyMatrices(dotInv, dot2)); - } - function evaluatePolynomial(coeffs, x, order) { - return coeffs.reduce((acc, coeff, i) => acc + coeff * Math.pow(x, order - i), 0); - } - function expM(M) { - return M.map((col) => col.map((cell) => Math.exp(cell))); - } - function logM(M) { - return M.map((col) => col.map((cell) => Math.log(cell))); - } - function predictLinearValues(Y, X, newX, computeIntercept) { - const { _X, _newX } = prepareDataForRegression(X, Y, newX); - const coeffs = fullLinearRegression(_X, Y, computeIntercept, false); - const nVar = coeffs.length - 1; - const newY = _newX.map((col) => { - let value = 0; - for (let i = 0; i < nVar; i++) { - value += coeffs[i][0] * col[nVar - i - 1]; - } - value += coeffs[nVar][0]; - return [value]; - }); - return newY.length === newX.length ? newY : transposeMatrix(newY); - } - // ----------------------------------------------------------------------------- - // AVEDEV - // ----------------------------------------------------------------------------- - const AVEDEV = { - description: _t("Average magnitude of deviations from mean."), - args: [ - arg("value1 (number, range)", _t("The first value or range of the sample.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the sample.")), - ], - returns: ["NUMBER"], - compute: function (...values) { - let count = 0; - const sum = reduceNumbers(values, (acc, a) => { - count += 1; - return acc + a; - }, 0, this.locale); - assert(() => count !== 0, _t(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`)); - const average = sum / count; - return reduceNumbers(values, (acc, a) => acc + Math.abs(average - a), 0, this.locale) / count; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // AVERAGE - // ----------------------------------------------------------------------------- - const AVERAGE = { - description: _t(`Numerical average value in a dataset, ignoring text.`), - args: [ - arg("value1 (number, range)", _t("The first value or range to consider when calculating the average value.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to consider when calculating the average value.")), - ], - returns: ["NUMBER"], - computeFormat: (value1) => { - return isMatrix(value1) ? value1[0][0]?.format : value1?.format; - }, - compute: function (...values) { - let count = 0; - const sum = reduceNumbers(values, (acc, a) => { - count += 1; - return acc + a; - }, 0, this.locale); - assert(() => count !== 0, _t(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`)); - return sum / count; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // AVERAGE.WEIGHTED - // ----------------------------------------------------------------------------- - const rangeError = _t(`[[FUNCTION_NAME]] has mismatched range sizes.`); - const negativeWeightError = _t(`[[FUNCTION_NAME]] expects the weight to be positive or equal to 0.`); - const AVERAGE_WEIGHTED = { - description: _t(`Weighted average.`), - args: [ - arg("values (number, range)", _t("Values to average.")), - arg("weights (number, range)", _t("Weights for each corresponding value.")), - arg("additional_values (number, range, repeating)", _t("Additional values to average.")), - arg("additional_weights (number, range, repeating)", _t("Additional weights.")), - ], - returns: ["NUMBER"], - computeFormat: (values) => { - return isMatrix(values) ? values[0][0]?.format : values?.format; - }, - compute: function (...values) { - let sum = 0; - let count = 0; - let value; - let weight; - assert(() => values.length % 2 === 0, _t(`Wrong number of Argument[]. Expected an even number of Argument[].`)); - for (let n = 0; n < values.length - 1; n += 2) { - value = values[n]; - weight = values[n + 1]; - // if (typeof value != typeof weight) { - // throw new Error(rangeError); - // } - if (isMatrix(value)) { - assert(() => isMatrix(weight), rangeError); - let dimColValue = value.length; - let dimLinValue = value[0].length; - assert(() => dimColValue === weight.length && dimLinValue === weight[0].length, rangeError); - for (let i = 0; i < dimColValue; i++) { - for (let j = 0; j < dimLinValue; j++) { - let subValue = value[i][j]; - let subWeight = weight[i][j]; - let subValueIsNumber = typeof subValue === "number"; - let subWeightIsNumber = typeof subWeight === "number"; - // typeof subValue or subWeight can be 'number' or 'undefined' - assert(() => subValueIsNumber === subWeightIsNumber, _t(`[[FUNCTION_NAME]] expects number values.`)); - if (subWeightIsNumber) { - assert(() => subWeight >= 0, negativeWeightError); - sum += subValue * subWeight; - count += subWeight; - } - } - } - } - else { - weight = toNumber(weight, this.locale); - value = toNumber(value, this.locale); - assert(() => weight >= 0, negativeWeightError); - sum += value * weight; - count += weight; - } - } - assert(() => count !== 0, _t(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`)); - return sum / count; - }, - }; - // ----------------------------------------------------------------------------- - // AVERAGEA - // ----------------------------------------------------------------------------- - const AVERAGEA = { - description: _t(`Numerical average value in a dataset.`), - args: [ - arg("value1 (number, range)", _t("The first value or range to consider when calculating the average value.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to consider when calculating the average value.")), - ], - returns: ["NUMBER"], - computeFormat: (value1) => { - return isMatrix(value1) ? value1[0][0]?.format : value1?.format; - }, - compute: function (...values) { - let count = 0; - const sum = reduceNumbersTextAs0(values, (acc, a) => { - count += 1; - return acc + a; - }, 0, this.locale); - assert(() => count !== 0, _t(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`)); - return sum / count; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // AVERAGEIF - // ----------------------------------------------------------------------------- - const AVERAGEIF = { - description: _t(`Average of values depending on criteria.`), - args: [ - arg("criteria_range (number, range)", _t("The range to check against criterion.")), - arg("criterion (string)", _t("The pattern or test to apply to criteria_range.")), - arg("average_range (number, range, default=criteria_range)", _t("The range to average. If not included, criteria_range is used for the average instead.")), - ], - returns: ["NUMBER"], - compute: function (criteriaRange, criterion, averageRange) { - const _criteriaRange = toMatrix(criteriaRange); - const _averageRange = averageRange === undefined ? _criteriaRange : toMatrix(averageRange); - let count = 0; - let sum = 0; - visitMatchingRanges([criteriaRange, criterion], (i, j) => { - const value = _averageRange[i][j]; - if (typeof value === "number") { - count += 1; - sum += value; - } - }, this.locale); - assert(() => count !== 0, _t(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`)); - return sum / count; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // AVERAGEIFS - // ----------------------------------------------------------------------------- - const AVERAGEIFS = { - description: _t(`Average of values depending on multiple criteria.`), - args: [ - arg("average_range (range)", _t("The range to average.")), - arg("criteria_range1 (range)", _t("The range to check against criterion1.")), - arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1.")), - arg("criteria_range2 (any, range, repeating)", _t("Additional criteria_range and criterion to check.")), - arg("criterion2 (string, repeating)", _t("The pattern or test to apply to criteria_range2.")), - ], - returns: ["NUMBER"], - compute: function (averageRange, ...values) { - const _averageRange = toMatrix(averageRange); - let count = 0; - let sum = 0; - visitMatchingRanges(values, (i, j) => { - const value = _averageRange[i][j]; - if (typeof value === "number") { - count += 1; - sum += value; - } - }, this.locale); - assert(() => count !== 0, _t(`Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.`)); - return sum / count; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COUNT - // ----------------------------------------------------------------------------- - const COUNT = { - description: _t(`The number of numeric values in dataset.`), - args: [ - arg("value1 (number, range)", _t("The first value or range to consider when counting.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to consider when counting.")), - ], - returns: ["NUMBER"], - compute: function (...values) { - let count = 0; - for (let n of values) { - if (isMatrix(n)) { - for (let i of n) { - for (let j of i) { - if (typeof j === "number") { - count += 1; - } - } - } - } - else if (typeof n !== "string" || - isNumber(n, this.locale) || - parseDateTime(n, this.locale)) { - count += 1; - } - } - return count; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COUNTA - // ----------------------------------------------------------------------------- - const COUNTA = { - description: _t(`The number of values in a dataset.`), - args: [ - arg("value1 (any, range)", _t("The first value or range to consider when counting.")), - arg("value2 (any, range, repeating)", _t("Additional values or ranges to consider when counting.")), - ], - returns: ["NUMBER"], - compute: function (...values) { - return reduceAny(values, (acc, a) => (a !== undefined && a !== null ? acc + 1 : acc), 0); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COVAR - // ----------------------------------------------------------------------------- - // Note: Unlike the VAR function which corresponds to the variance over a sample (VAR.S), - // the COVAR function corresponds to the covariance over an entire population (COVAR.P) - const COVAR = { - description: _t(`The covariance of a dataset.`), - args: [ - arg("data_y (any, range)", _t("The range representing the array or matrix of dependent data.")), - arg("data_x (any, range)", _t("The range representing the array or matrix of independent data.")), - ], - returns: ["NUMBER"], - compute: function (dataY, dataX) { - return covariance(dataY, dataX, false); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COVARIANCE.P - // ----------------------------------------------------------------------------- - const COVARIANCE_P = { - description: _t(`The covariance of a dataset.`), - args: [ - arg("data_y (any, range)", _t("The range representing the array or matrix of dependent data.")), - arg("data_x (any, range)", _t("The range representing the array or matrix of independent data.")), - ], - returns: ["NUMBER"], - compute: function (dataY, dataX) { - return covariance(dataY, dataX, false); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COVARIANCE.S - // ----------------------------------------------------------------------------- - const COVARIANCE_S = { - description: _t(`The sample covariance of a dataset.`), - args: [ - arg("data_y (any, range)", _t("The range representing the array or matrix of dependent data.")), - arg("data_x (any, range)", _t("The range representing the array or matrix of independent data.")), - ], - returns: ["NUMBER"], - compute: function (dataY, dataX) { - return covariance(dataY, dataX, true); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // FORECAST - // ----------------------------------------------------------------------------- - const FORECAST = { - description: _t("Calculates the expected y-value for a specified x based on a linear regression of a dataset."), - args: [ - arg("x (number, range)", _t("The value(s) on the x-axis to forecast.")), - arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), - arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), - ], - returns: ["NUMBER"], - compute: function (x, dataY, dataX) { - const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX); - return predictLinearValues([flatDataY], [flatDataX], matrixMap(toMatrix(x), (value) => toNumber(value, this.locale)), true); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // GROWTH - // ----------------------------------------------------------------------------- - const GROWTH = { - description: _t("Fits points to exponential growth trend."), - args: [ - arg("known_data_y (range)", _t("The array or range containing dependent (y) values that are already known, used to curve fit an ideal exponential growth curve.")), - arg("known_data_x (range, default={1;2;3;...})", _t("The values of the independent variable(s) corresponding with known_data_y.")), - arg("new_data_x (any, range, default=known_data_x)", _t("The data points to return the y values for on the ideal curve fit.")), - arg("b (boolean, default=TRUE)", _t("Given a general exponential form of y = b*m^x for a curve fit, calculates b if TRUE or forces b to be 1 and only calculates the m values if FALSE.")), - ], - returns: ["NUMBER"], - compute: function (knownDataY, knownDataX = [], newDataX = [], b = true) { - return expM(predictLinearValues(logM(tryCastAsNumberMatrix(knownDataY, "the first argument (known_data_y)")), tryCastAsNumberMatrix(knownDataX, "the second argument (known_data_x)"), tryCastAsNumberMatrix(newDataX, "the third argument (new_data_y)"), toBoolean(b))); - }, - }; - // ----------------------------------------------------------------------------- - // INTERCEPT - // ----------------------------------------------------------------------------- - const INTERCEPT = { - description: _t("Compute the intercept of the linear regression."), - args: [ - arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), - arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), - ], - returns: ["NUMBER"], - compute: function (dataY, dataX) { - const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX); - const [[], [intercept]] = fullLinearRegression([flatDataX], [flatDataY]); - return intercept; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // LARGE - // ----------------------------------------------------------------------------- - const LARGE = { - description: _t("Nth largest element from a data set."), - args: [ - arg("data (any, range)", _t("Array or range containing the dataset to consider.")), - arg("n (number)", _t("The rank from largest to smallest of the element to return.")), - ], - returns: ["NUMBER"], - computeValueAndFormat: function (data, n) { - const _n = Math.trunc(toNumber(n?.value, this.locale)); - let largests = []; - let index; - let count = 0; - visitAny([data], (d) => { - if (typeof d.value === "number") { - index = dichotomicSearch(largests, d.value, "nextSmaller", "asc", largests.length, (array, i) => array[i].value); - largests.splice(index + 1, 0, d); - count++; - if (count > _n) { - largests.shift(); - count--; - } - } - }); - const result = largests.shift(); - assert(() => result !== undefined, _t(`[[FUNCTION_NAME]] has no valid input data.`)); - assert(() => count >= _n, _t("Function [[FUNCTION_NAME]] parameter 2 value (%s) is out of range.", _n)); - return result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // LINEST - // ----------------------------------------------------------------------------- - const LINEST = { - description: _t("Compute the intercept of the linear regression."), - args: [ - arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), - arg("data_x (range, default={1;2;3;...})", _t("The range representing the array or matrix of independent data.")), - arg("calculate_b (boolean, default=TRUE)", _t("A flag specifying wheter to compute the slope or not")), - arg("verbose (boolean, default=FALSE)", _t("A flag specifying whether to return additional regression statistics or only the linear coefficients and the y-intercept")), - ], - returns: ["NUMBER"], - compute: function (dataY, dataX = [], calculateB = true, verbose = false) { - return fullLinearRegression(tryCastAsNumberMatrix(dataX, "the first argument (data_y)"), tryCastAsNumberMatrix(dataY, "the second argument (data_x)"), toBoolean(calculateB), toBoolean(verbose)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // LOGEST - // ----------------------------------------------------------------------------- - const LOGEST = { - description: _t("Compute the intercept of the linear regression."), - args: [ - arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), - arg("data_x (range, optional, default={1;2;3;...})", _t("The range representing the array or matrix of independent data.")), - arg("calculate_b (boolean, default=TRUE)", _t("A flag specifying wheter to compute the slope or not")), - arg("verbose (boolean, default=FALSE)", _t("A flag specifying whether to return additional regression statistics or only the linear coefficients and the y-intercept")), - ], - returns: ["NUMBER"], - compute: function (dataY, dataX = [], calculateB = true, verbose = false) { - const coeffs = fullLinearRegression(tryCastAsNumberMatrix(dataX, "the second argument (data_x)"), logM(tryCastAsNumberMatrix(dataY, "the first argument (data_y)")), toBoolean(calculateB), toBoolean(verbose)); - for (let i = 0; i < coeffs.length; i++) { - coeffs[i][0] = Math.exp(coeffs[i][0]); - } - return coeffs; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MATTHEWS - // ----------------------------------------------------------------------------- - const MATTHEWS = { - description: _t("Compute the Matthews correlation coefficient of a dataset."), - args: [ - arg("data_x (range)", _t("The range representing the array or matrix of observed data.")), - arg("data_y (range)", _t("The range representing the array or matrix of predicted data.")), - ], - returns: ["NUMBER"], - compute: function (dataX, dataY) { - const flatX = dataX.flat(); - const flatY = dataY.flat(); - assertSameNumberOfElements(flatX, flatY); - if (flatX.length === 0) { - throw new Error(_t("[[FUNCTION_NAME]] expects non-empty ranges for both parameters.")); - } - const n = flatX.length; - let trueN = 0, trueP = 0, falseP = 0, falseN = 0; - for (let i = 0; i < n; ++i) { - const isTrue1 = toBoolean(flatX[i]); - const isTrue2 = toBoolean(flatY[i]); - if (isTrue1 === isTrue2) { - if (isTrue1) { - trueP++; - } - else { - trueN++; - } - } - else { - if (isTrue1) { - falseN++; - } - else { - falseP++; - } - } - } - return ((trueP * trueN - falseP * falseN) / - Math.sqrt((trueP + falseP) * (trueP + falseN) * (trueN + falseP) * (trueN + falseN))); - }, - isExported: false, - }; - // ----------------------------------------------------------------------------- - // MAX - // ----------------------------------------------------------------------------- - const MAX = { - description: _t("Maximum value in a numeric dataset."), - args: [ - arg("value1 (number, range)", _t("The first value or range to consider when calculating the maximum value.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to consider when calculating the maximum value.")), - ], - returns: ["NUMBER"], - computeFormat: (value1) => { - return isMatrix(value1) ? value1[0][0]?.format : value1?.format; - }, - compute: function (...values) { - const result = reduceNumbers(values, (acc, a) => (acc < a ? a : acc), -Infinity, this.locale); - return result === -Infinity ? 0 : result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MAXA - // ----------------------------------------------------------------------------- - const MAXA = { - description: _t("Maximum numeric value in a dataset."), - args: [ - arg("value1 (any, range)", _t("The first value or range to consider when calculating the maximum value.")), - arg("value2 (any, range, repeating)", _t("Additional values or ranges to consider when calculating the maximum value.")), - ], - returns: ["NUMBER"], - computeFormat: (value1) => { - return isMatrix(value1) ? value1[0][0]?.format : value1?.format; - }, - compute: function (...values) { - const maxa = reduceNumbersTextAs0(values, (acc, a) => { - return Math.max(a, acc); - }, -Infinity, this.locale); - return maxa === -Infinity ? 0 : maxa; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MAXIFS - // ----------------------------------------------------------------------------- - const MAXIFS = { - description: _t("Returns the maximum value in a range of cells, filtered by a set of criteria."), - args: [ - arg("range (range)", _t("The range of cells from which the maximum will be determined.")), - arg("criteria_range1 (range)", _t("The range of cells over which to evaluate criterion1.")), - arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.")), - arg("criteria_range2 (any, range, repeating)", _t("Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.")), - arg("criterion2 (string, repeating)", _t("The pattern or test to apply to criteria_range2.")), - ], - returns: ["NUMBER"], - compute: function (range, ...args) { - let result = -Infinity; - const _range = toMatrix(range); - visitMatchingRanges(args, (i, j) => { - const value = _range[i][j]; - if (typeof value === "number") { - result = result < value ? value : result; - } - }, this.locale); - return result === -Infinity ? 0 : result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MEDIAN - // ----------------------------------------------------------------------------- - const MEDIAN = { - description: _t("Median value in a numeric dataset."), - args: [ - arg("value1 (any, range)", _t("The first value or range to consider when calculating the median value.")), - arg("value2 (any, range, repeating)", _t("Additional values or ranges to consider when calculating the median value.")), - ], - returns: ["NUMBER"], - computeFormat: (value1) => { - return isMatrix(value1) ? value1[0][0]?.format : value1?.format; - }, - compute: function (...values) { - let data = []; - visitNumbers(values, (arg) => { - data.push(arg); - }, this.locale); - return centile(data, 0.5, true, this.locale); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MIN - // ----------------------------------------------------------------------------- - const MIN = { - description: _t("Minimum value in a numeric dataset."), - args: [ - arg("value1 (number, range)", _t("The first value or range to consider when calculating the minimum value.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to consider when calculating the minimum value.")), - ], - returns: ["NUMBER"], - computeFormat: (value1) => { - return isMatrix(value1) ? value1[0][0]?.format : value1?.format; - }, - compute: function (...values) { - const result = reduceNumbers(values, (acc, a) => (a < acc ? a : acc), Infinity, this.locale); - return result === Infinity ? 0 : result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MINA - // ----------------------------------------------------------------------------- - const MINA = { - description: _t("Minimum numeric value in a dataset."), - args: [ - arg("value1 (number, range)", _t("The first value or range to consider when calculating the minimum value.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to consider when calculating the minimum value.")), - ], - returns: ["NUMBER"], - computeFormat: (value1) => { - return isMatrix(value1) ? value1[0][0]?.format : value1?.format; - }, - compute: function (...values) { - const mina = reduceNumbersTextAs0(values, (acc, a) => { - return Math.min(a, acc); - }, Infinity, this.locale); - return mina === Infinity ? 0 : mina; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MINIFS - // ----------------------------------------------------------------------------- - const MINIFS = { - description: _t("Returns the minimum value in a range of cells, filtered by a set of criteria."), - args: [ - arg("range (range)", _t("The range of cells from which the minimum will be determined.")), - arg("criteria_range1 (range)", _t("The range of cells over which to evaluate criterion1.")), - arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.")), - arg("criteria_range2 (any, range, repeating)", _t("Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.")), - arg("criterion2 (string, repeating)", _t("The pattern or test to apply to criteria_range2.")), - ], - returns: ["NUMBER"], - compute: function (range, ...args) { - let result = Infinity; - const _range = toMatrix(range); - visitMatchingRanges(args, (i, j) => { - const value = _range[i][j]; - if (typeof value === "number") { - result = result > value ? value : result; - } - }, this.locale); - return result === Infinity ? 0 : result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // PEARSON - // ----------------------------------------------------------------------------- - const PEARSON = { - description: _t("Compute the Pearson product-moment correlation coefficient of a dataset."), - args: [ - arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), - arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), - ], - returns: ["NUMBER"], - compute: function (dataY, dataX) { - const { flatDataX, flatDataY } = filterAndFlatData(dataX, dataY); - if (flatDataX.length === 0) { - throw new Error(_t("[[FUNCTION_NAME]] expects non-empty ranges for both parameters.")); - } - if (flatDataX.length < 2) { - throw new Error(_t("[[FUNCTION_NAME]] needs at least two values for both parameters")); - } - const n = flatDataX.length; - let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0, sumYY = 0; - for (let i = 0; i < n; i++) { - const xij = flatDataX[i]; - const yij = flatDataY[i]; - sumX += xij; - sumY += yij; - sumXY += xij * yij; - sumXX += xij * xij; - sumYY += yij * yij; - } - return ((n * sumXY - sumX * sumY) / Math.sqrt((n * sumXX - sumX * sumX) * (n * sumYY - sumY * sumY))); - }, - isExported: true, - }; - // In GSheet, CORREL is just an alias to PEARSON - const CORREL = PEARSON; - // ----------------------------------------------------------------------------- - // PERCENTILE - // ----------------------------------------------------------------------------- - const PERCENTILE = { - description: _t("Value at a given percentile of a dataset."), - args: [ - arg("data (any, range)", _t("The array or range containing the dataset to consider.")), - arg("percentile (number)", _t("The percentile whose value within data will be calculated and returned.")), - ], - returns: ["NUMBER"], - computeFormat: (data) => { - return isMatrix(data) ? data[0][0]?.format : data?.format; - }, - compute: function (data, percentile) { - return PERCENTILE_INC.compute.bind(this)(data, percentile); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // PERCENTILE.EXC - // ----------------------------------------------------------------------------- - const PERCENTILE_EXC = { - description: _t("Value at a given percentile of a dataset exclusive of 0 and 1."), - args: [ - arg("data (any, range)", _t("The array or range containing the dataset to consider.")), - arg("percentile (number)", _t("The percentile, exclusive of 0 and 1, whose value within 'data' will be calculated and returned.")), - ], - returns: ["NUMBER"], - computeFormat: (data) => { - return isMatrix(data) ? data[0][0]?.format : data?.format; - }, - compute: function (data, percentile) { - return centile([data], percentile, false, this.locale); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // PERCENTILE.INC - // ----------------------------------------------------------------------------- - const PERCENTILE_INC = { - description: _t("Value at a given percentile of a dataset."), - args: [ - arg("data (any, range)", _t("The array or range containing the dataset to consider.")), - arg("percentile (number)", _t("The percentile whose value within data will be calculated and returned.")), - ], - returns: ["NUMBER"], - computeFormat: (data) => { - return isMatrix(data) ? data[0][0]?.format : data?.format; - }, - compute: function (data, percentile) { - return centile([data], percentile, true, this.locale); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // POLYFIT - // ----------------------------------------------------------------------------- - const POLYFIT_COEFFS = { - description: _t("Compute the coefficients of polynomial regression of the dataset."), - args: [ - arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), - arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), - arg("order (number)", _t("The order of the polynomial to fit the data, between 1 and 6.")), - arg("intercept (boolean, default=TRUE)", _t("A flag specifying whether to compute the intercept or not.")), - ], - returns: ["RANGE"], - compute: function (dataY, dataX, order, intercept = true) { - const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX); - return polynomialRegression(flatDataY, flatDataX, toNumber(order, this.locale), toBoolean(intercept)); - }, - isExported: false, - }; - // ----------------------------------------------------------------------------- - // POLYFIT.FORECAST - // ----------------------------------------------------------------------------- - const POLYFIT_FORECAST = { - description: _t("Predict value by computing a polynomial regression of the dataset."), - args: [ - arg("x (number, range)", _t("The value(s) on the x-axis to forecast.")), - arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), - arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), - arg("order (number)", _t("The order of the polynomial to fit the data, between 1 and 6.")), - arg("intercept (boolean, default=TRUE)", _t("A flag specifying whether to compute the intercept or not.")), - ], - returns: ["NUMBER"], - compute: function (x, dataY, dataX, order, intercept = true) { - const _order = toNumber(order, this.locale); - const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX); - const coeffs = polynomialRegression(flatDataY, flatDataX, _order, toBoolean(intercept)).flat(); - return matrixMap(toMatrix(x), (xij) => evaluatePolynomial(coeffs, toNumber(xij, this.locale), _order)); - }, - isExported: false, - }; - // ----------------------------------------------------------------------------- - // QUARTILE - // ----------------------------------------------------------------------------- - const QUARTILE = { - description: _t("Value nearest to a specific quartile of a dataset."), - args: [ - arg("data (any, range)", _t("The array or range containing the dataset to consider.")), - arg("quartile_number (number)", _t("Which quartile value to return.")), - ], - returns: ["NUMBER"], - computeFormat: (data) => { - return isMatrix(data) ? data[0][0]?.format : data?.format; - }, - compute: function (data, quartileNumber) { - return QUARTILE_INC.compute.bind(this)(data, quartileNumber); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // QUARTILE.EXC - // ----------------------------------------------------------------------------- - const QUARTILE_EXC = { - description: _t("Value nearest to a specific quartile of a dataset exclusive of 0 and 4."), - args: [ - arg("data (any, range)", _t("The array or range containing the dataset to consider.")), - arg("quartile_number (number)", _t("Which quartile value, exclusive of 0 and 4, to return.")), - ], - returns: ["NUMBER"], - computeFormat: (data) => { - return isMatrix(data) ? data[0][0]?.format : data?.format; - }, - compute: function (data, quartileNumber) { - const _quartileNumber = Math.trunc(toNumber(quartileNumber, this.locale)); - return centile([data], 0.25 * _quartileNumber, false, this.locale); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // QUARTILE.INC - // ----------------------------------------------------------------------------- - const QUARTILE_INC = { - description: _t("Value nearest to a specific quartile of a dataset."), - args: [ - arg("data (any, range)", _t("The array or range containing the dataset to consider.")), - arg("quartile_number (number)", _t("Which quartile value to return.")), - ], - returns: ["NUMBER"], - computeFormat: (data) => { - return isMatrix(data) ? data[0][0]?.format : data?.format; - }, - compute: function (data, quartileNumber) { - const _quartileNumber = Math.trunc(toNumber(quartileNumber, this.locale)); - return centile([data], 0.25 * _quartileNumber, true, this.locale); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // RSQ - // ----------------------------------------------------------------------------- - const RSQ = { - description: _t("Compute the square of r, the Pearson product-moment correlation coefficient of a dataset."), - args: [ - arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), - arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), - ], - returns: ["NUMBER"], - compute: function (dataY, dataX) { - const pearson = PEARSON.compute.bind(this); - return Math.pow(pearson(dataX, dataY), 2.0); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SLOPE - // ----------------------------------------------------------------------------- - const SLOPE = { - description: _t("Compute the slope of the linear regression."), - args: [ - arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), - arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), - ], - returns: ["NUMBER"], - compute: function (dataY, dataX) { - const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX); - const [[slope]] = fullLinearRegression([flatDataX], [flatDataY]); - return slope; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SMALL - // ----------------------------------------------------------------------------- - const SMALL = { - description: _t("Nth smallest element in a data set."), - args: [ - arg("data (any, range)", _t("The array or range containing the dataset to consider.")), - arg("n (number)", _t("The rank from smallest to largest of the element to return.")), - ], - returns: ["NUMBER"], - computeValueAndFormat: function (data, n) { - const _n = Math.trunc(toNumber(n?.value, this.locale)); - let largests = []; - let index; - let count = 0; - visitAny([data], (d) => { - if (typeof d.value === "number") { - index = dichotomicSearch(largests, d.value, "nextSmaller", "asc", largests.length, (array, i) => array[i].value); - largests.splice(index + 1, 0, d); - count++; - if (count > _n) { - largests.pop(); - count--; - } - } - }); - const result = largests.pop(); - assert(() => result !== undefined, _t(`[[FUNCTION_NAME]] has no valid input data.`)); - assert(() => count >= _n, _t("Function [[FUNCTION_NAME]] parameter 2 value (%s) is out of range.", _n)); - return result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SPEARMAN - // ----------------------------------------------------------------------------- - const SPEARMAN = { - description: _t("Compute the Spearman rank correlation coefficient of a dataset."), - args: [ - arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), - arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), - ], - returns: ["NUMBER"], - compute: function (dataX, dataY) { - const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX); - const n = flatDataX.length; - const order = flatDataX.map((e, i) => [e, flatDataY[i]]); - order.sort((a, b) => a[0] - b[0]); - for (let i = 0; i < n; ++i) { - order[i][0] = i; - } - order.sort((a, b) => a[1] - b[1]); - let sum = 0.0; - for (let i = 0; i < n; ++i) { - sum += (order[i][0] - i) ** 2; - } - return 1 - (6 * sum) / (n ** 3 - n); - }, - isExported: false, - }; - // ----------------------------------------------------------------------------- - // STDEV - // ----------------------------------------------------------------------------- - const STDEV = { - description: _t("Standard deviation."), - args: [ - arg("value1 (number, range)", _t("The first value or range of the sample.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the sample.")), - ], - returns: ["NUMBER"], - compute: function (...values) { - return Math.sqrt(VAR.compute.bind(this)(...values)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // STDEV.P - // ----------------------------------------------------------------------------- - const STDEV_P = { - description: _t("Standard deviation of entire population."), - args: [ - arg("value1 (number, range)", _t("The first value or range of the population.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the population.")), - ], - returns: ["NUMBER"], - compute: function (...values) { - return Math.sqrt(VAR_P.compute.bind(this)(...values)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // STDEV.S - // ----------------------------------------------------------------------------- - const STDEV_S = { - description: _t("Standard deviation."), - args: [ - arg("value1 (number, range)", _t("The first value or range of the sample.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the sample.")), - ], - returns: ["NUMBER"], - compute: function (...values) { - return Math.sqrt(VAR_S.compute.bind(this)(...values)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // STDEVA - // ----------------------------------------------------------------------------- - const STDEVA = { - description: _t("Standard deviation of sample (text as 0)."), - args: [ - arg("value1 (number, range)", _t("The first value or range of the sample.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the sample.")), - ], - returns: ["NUMBER"], - compute: function (...values) { - return Math.sqrt(VARA.compute.bind(this)(...values)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // STDEVP - // ----------------------------------------------------------------------------- - const STDEVP = { - description: _t("Standard deviation of entire population."), - args: [ - arg("value1 (number, range)", _t("The first value or range of the population.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the population.")), - ], - returns: ["NUMBER"], - compute: function (...values) { - return Math.sqrt(VARP.compute.bind(this)(...values)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // STDEVPA - // ----------------------------------------------------------------------------- - const STDEVPA = { - description: _t("Standard deviation of entire population (text as 0)."), - args: [ - arg("value1 (number, range)", _t("The first value or range of the population.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the population.")), - ], - returns: ["NUMBER"], - compute: function (...values) { - return Math.sqrt(VARPA.compute.bind(this)(...values)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // STEYX - // ----------------------------------------------------------------------------- - const STEYX = { - description: _t("Calculates the standard error of the predicted y-value for each x in the regression of a dataset."), - args: [ - arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), - arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), - ], - returns: ["NUMBER"], - compute: function (dataY, dataX) { - const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX); - const data = fullLinearRegression([flatDataX], [flatDataY], true, true); - return data[1][2]; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TREND - // ----------------------------------------------------------------------------- - const TREND = { - description: _t("Fits points to linear trend derived via least-squares."), - args: [ - arg("known_data_y (number, range)", _t("The array or range containing dependent (y) values that are already known, used to curve fit an ideal linear trend.")), - arg("known_data_x (number, range, optional, default={1;2;3;...})", _t("The values of the independent variable(s) corresponding with known_data_y.")), - arg("new_data_x (number, range, optional, default=known_data_x)", _t("The data points to return the y values for on the ideal curve fit.")), - arg("b (boolean, optional, default=TRUE)", _t("Given a general linear form of y = m*x+b for a curve fit, calculates b if TRUE or forces b to be 0 and only calculates the m values if FALSE, i.e. forces the curve fit to pass through the origin.")), - ], - returns: ["NUMBER"], - compute: function (knownDataY, knownDataX = [], newDataX = [], b = true) { - return predictLinearValues(tryCastAsNumberMatrix(knownDataY, "the first argument (known_data_y)"), tryCastAsNumberMatrix(knownDataX, "the second argument (known_data_x)"), tryCastAsNumberMatrix(newDataX, "the third argument (new_data_y)"), toBoolean(b)); - }, - }; - // ----------------------------------------------------------------------------- - // VAR - // ----------------------------------------------------------------------------- - const VAR = { - description: _t("Variance."), - args: [ - arg("value1 (number, range)", _t("The first value or range of the sample.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the sample.")), - ], - returns: ["NUMBER"], - compute: function (...values) { - return variance(values, true, false, this.locale); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // VAR.P - // ----------------------------------------------------------------------------- - const VAR_P = { - description: _t("Variance of entire population."), - args: [ - arg("value1 (number, range)", _t("The first value or range of the population.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the population.")), - ], - returns: ["NUMBER"], - compute: function (...values) { - return variance(values, false, false, this.locale); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // VAR.S - // ----------------------------------------------------------------------------- - const VAR_S = { - description: _t("Variance."), - args: [ - arg("value1 (number, range)", _t("The first value or range of the sample.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the sample.")), - ], - returns: ["NUMBER"], - compute: function (...values) { - return variance(values, true, false, this.locale); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // VARA - // ----------------------------------------------------------------------------- - const VARA = { - description: _t("Variance of sample (text as 0)."), - args: [ - arg("value1 (number, range)", _t("The first value or range of the sample.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the sample.")), - ], - returns: ["NUMBER"], - compute: function (...values) { - return variance(values, true, true, this.locale); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // VARP - // ----------------------------------------------------------------------------- - const VARP = { - description: _t("Variance of entire population."), - args: [ - arg("value1 (number, range)", _t("The first value or range of the population.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the population.")), - ], - returns: ["NUMBER"], - compute: function (...values) { - return variance(values, false, false, this.locale); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // VARPA - // ----------------------------------------------------------------------------- - const VARPA = { - description: _t("Variance of entire population (text as 0)."), - args: [ - arg("value1 (number, range)", _t("The first value or range of the population.")), - arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the population.")), - ], - returns: ["NUMBER"], - compute: function (...values) { - return variance(values, false, true, this.locale); - }, - isExported: true, - }; - - var statistical = /*#__PURE__*/Object.freeze({ - __proto__: null, - AVEDEV: AVEDEV, - AVERAGE: AVERAGE, - AVERAGEA: AVERAGEA, - AVERAGEIF: AVERAGEIF, - AVERAGEIFS: AVERAGEIFS, - AVERAGE_WEIGHTED: AVERAGE_WEIGHTED, - CORREL: CORREL, - COUNT: COUNT, - COUNTA: COUNTA, - COVAR: COVAR, - COVARIANCE_P: COVARIANCE_P, - COVARIANCE_S: COVARIANCE_S, - FORECAST: FORECAST, - GROWTH: GROWTH, - INTERCEPT: INTERCEPT, - LARGE: LARGE, - LINEST: LINEST, - LOGEST: LOGEST, - MATTHEWS: MATTHEWS, - MAX: MAX, - MAXA: MAXA, - MAXIFS: MAXIFS, - MEDIAN: MEDIAN, - MIN: MIN, - MINA: MINA, - MINIFS: MINIFS, - PEARSON: PEARSON, - PERCENTILE: PERCENTILE, - PERCENTILE_EXC: PERCENTILE_EXC, - PERCENTILE_INC: PERCENTILE_INC, - POLYFIT_COEFFS: POLYFIT_COEFFS, - POLYFIT_FORECAST: POLYFIT_FORECAST, - QUARTILE: QUARTILE, - QUARTILE_EXC: QUARTILE_EXC, - QUARTILE_INC: QUARTILE_INC, - RSQ: RSQ, - SLOPE: SLOPE, - SMALL: SMALL, - SPEARMAN: SPEARMAN, - STDEV: STDEV, - STDEVA: STDEVA, - STDEVP: STDEVP, - STDEVPA: STDEVPA, - STDEV_P: STDEV_P, - STDEV_S: STDEV_S, - STEYX: STEYX, - TREND: TREND, - VAR: VAR, - VARA: VARA, - VARP: VARP, - VARPA: VARPA, - VAR_P: VAR_P, - VAR_S: VAR_S - }); - - function getMatchingCells(database, field, criteria, locale) { - // Example - // # DATABASE # CRITERIA # field = "C" - // - // | A | B | C | | A | C | - // |===========| |=======| - // | 1 | x | j | |<2 | j | - // | 1 | Z | k | | | 7 | - // | 5 | y | 7 | - // 1 - Select coordinates of database columns ---------------------------------------------------- - const indexColNameDB = new Map(); - const dimRowDB = database.length; - for (let indexCol = dimRowDB - 1; indexCol >= 0; indexCol--) { - indexColNameDB.set(toString(database[indexCol][0]).toUpperCase(), indexCol); - } - // Example continuation: indexColNameDB = {"A" => 0, "B" => 1, "C" => 2} - // 2 - Check if the field parameter exists in the column names of the database ------------------- - // field may either be a text label corresponding to a column header in the - // first row of database or a numeric index indicating which column to consider, - // where the first column has the value 1. - if (typeof field !== "number" && typeof field !== "string") { - throw new Error(_t("The field must be a number or a string")); - } - let index; - if (typeof field === "number") { - index = Math.trunc(field) - 1; - if (index < 0 || dimRowDB - 1 < index) { - throw new Error(_t("The field (%s) must be one of %s or must be a number between 1 and %s inclusive.", field.toString(), dimRowDB.toString())); - } - } - else { - const colName = toString(field).toUpperCase(); - index = indexColNameDB.get(colName) ?? -1; - if (index === -1) { - throw new Error(_t("The field (%s) must be one of %s.", toString(field), [...indexColNameDB.keys()].toString())); - } - } - // Example continuation: index = 2 - // 3 - For each criteria row, find database row that correspond ---------------------------------- - const dimColCriteria = criteria[0].length; - if (dimColCriteria < 2) { - throw new Error(_t("The criteria range contains %s row, it must be at least 2 rows.", dimColCriteria.toString())); - } - let matchingRows = new Set(); - const dimColDB = database[0].length; - for (let indexRow = 1; indexRow < dimColCriteria; indexRow++) { - let args = []; - let existColNameDB = true; - for (let indexCol = 0; indexCol < criteria.length; indexCol++) { - const currentName = toString(criteria[indexCol][0]).toUpperCase(); - const indexColDB = indexColNameDB.get(currentName); - const criter = criteria[indexCol][indexRow]; - if (criter !== null) { - if (indexColDB !== undefined) { - args.push([database[indexColDB].slice(1, dimColDB)]); - args.push(criter); - } - else { - existColNameDB = false; - break; - } - } - } - // Example continuation: args1 = [[1,1,5], "<2", ["j","k",7], "j"] - // Example continuation: args2 = [["j","k",7], "7"] - if (existColNameDB) { - if (args.length > 0) { - visitMatchingRanges(args, (i, j) => { - matchingRows.add(j); - }, locale, true); - } - else { - // return indices of each database row when a criteria table row is void - matchingRows = new Set(Array(dimColDB - 1).keys()); - break; - } - } - } - // Example continuation: matchingRows = {0, 2} - // 4 - return for each database row corresponding, the cells corresponding to the field parameter - const fieldCol = database[index]; - // Example continuation:: fieldCol = ["C", "j", "k", 7] - const matchingCells = [...matchingRows].map((x) => fieldCol[x + 1]); - // Example continuation:: matchingCells = ["j", 7] - return matchingCells; - } - const databaseArgs = [ - arg("database (range)", _t("The array or range containing the data to consider, structured in such a way that the first row contains the labels for each column's values.")), - arg("field (any)", _t("Indicates which column in database contains the values to be extracted and operated on.")), - arg("criteria (range)", _t("An array or range containing zero or more criteria to filter the database values by before operating.")), - ]; - // ----------------------------------------------------------------------------- - // DAVERAGE - // ----------------------------------------------------------------------------- - const DAVERAGE = { - description: _t("Average of a set of values from a table-like range."), - args: databaseArgs, - returns: ["NUMBER"], - compute: function (database, field, criteria) { - const cells = getMatchingCells(database, field, criteria, this.locale); - return AVERAGE.compute.bind(this)([cells]); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DCOUNT - // ----------------------------------------------------------------------------- - const DCOUNT = { - description: _t("Counts values from a table-like range."), - args: databaseArgs, - returns: ["NUMBER"], - compute: function (database, field, criteria) { - const cells = getMatchingCells(database, field, criteria, this.locale); - return COUNT.compute.bind(this)([cells]); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DCOUNTA - // ----------------------------------------------------------------------------- - const DCOUNTA = { - description: _t("Counts values and text from a table-like range."), - args: databaseArgs, - returns: ["NUMBER"], - compute: function (database, field, criteria) { - const cells = getMatchingCells(database, field, criteria, this.locale); - return COUNTA.compute.bind(this)([cells]); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DGET - // ----------------------------------------------------------------------------- - const DGET = { - description: _t("Single value from a table-like range."), - args: databaseArgs, - returns: ["NUMBER"], - compute: function (database, field, criteria) { - const cells = getMatchingCells(database, field, criteria, this.locale); - assert(() => cells.length === 1, _t("More than one match found in DGET evaluation.")); - return cells[0]; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DMAX - // ----------------------------------------------------------------------------- - const DMAX = { - description: _t("Maximum of values from a table-like range."), - args: databaseArgs, - returns: ["NUMBER"], - compute: function (database, field, criteria) { - const cells = getMatchingCells(database, field, criteria, this.locale); - return MAX.compute.bind(this)([cells]); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DMIN - // ----------------------------------------------------------------------------- - const DMIN = { - description: _t("Minimum of values from a table-like range."), - args: databaseArgs, - returns: ["NUMBER"], - compute: function (database, field, criteria) { - const cells = getMatchingCells(database, field, criteria, this.locale); - return MIN.compute.bind(this)([cells]); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DPRODUCT - // ----------------------------------------------------------------------------- - const DPRODUCT = { - description: _t("Product of values from a table-like range."), - args: databaseArgs, - returns: ["NUMBER"], - compute: function (database, field, criteria) { - const cells = getMatchingCells(database, field, criteria, this.locale); - return PRODUCT.compute.bind(this)([cells]); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DSTDEV - // ----------------------------------------------------------------------------- - const DSTDEV = { - description: _t("Standard deviation of population sample from table."), - args: databaseArgs, - returns: ["NUMBER"], - compute: function (database, field, criteria) { - const cells = getMatchingCells(database, field, criteria, this.locale); - return STDEV.compute.bind(this)([cells]); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DSTDEVP - // ----------------------------------------------------------------------------- - const DSTDEVP = { - description: _t("Standard deviation of entire population from table."), - args: databaseArgs, - returns: ["NUMBER"], - compute: function (database, field, criteria) { - const cells = getMatchingCells(database, field, criteria, this.locale); - return STDEVP.compute.bind(this)([cells]); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DSUM - // ----------------------------------------------------------------------------- - const DSUM = { - description: _t("Sum of values from a table-like range."), - args: databaseArgs, - returns: ["NUMBER"], - compute: function (database, field, criteria) { - const cells = getMatchingCells(database, field, criteria, this.locale); - return SUM.compute.bind(this)([cells]); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DVAR - // ----------------------------------------------------------------------------- - const DVAR = { - description: _t("Variance of population sample from table-like range."), - args: databaseArgs, - returns: ["NUMBER"], - compute: function (database, field, criteria) { - const cells = getMatchingCells(database, field, criteria, this.locale); - return VAR.compute.bind(this)([cells]); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DVARP - // ----------------------------------------------------------------------------- - const DVARP = { - description: _t("Variance of a population from a table-like range."), - args: databaseArgs, - returns: ["NUMBER"], - compute: function (database, field, criteria) { - const cells = getMatchingCells(database, field, criteria, this.locale); - return VARP.compute.bind(this)([cells]); - }, - isExported: true, - }; - - var database = /*#__PURE__*/Object.freeze({ - __proto__: null, - DAVERAGE: DAVERAGE, - DCOUNT: DCOUNT, - DCOUNTA: DCOUNTA, - DGET: DGET, - DMAX: DMAX, - DMIN: DMIN, - DPRODUCT: DPRODUCT, - DSTDEV: DSTDEV, - DSTDEVP: DSTDEVP, - DSUM: DSUM, - DVAR: DVAR, - DVARP: DVARP - }); - - const DEFAULT_TYPE = 1; - const DEFAULT_WEEKEND = 1; - var TIME_UNIT; - (function (TIME_UNIT) { - TIME_UNIT["WHOLE_YEARS"] = "Y"; - TIME_UNIT["WHOLE_MONTHS"] = "M"; - TIME_UNIT["WHOLE_DAYS"] = "D"; - TIME_UNIT["DAYS_WITHOUT_WHOLE_MONTHS"] = "MD"; - TIME_UNIT["MONTH_WITHOUT_WHOLE_YEARS"] = "YM"; - TIME_UNIT["DAYS_BETWEEN_NO_MORE_THAN_ONE_YEAR"] = "YD"; - })(TIME_UNIT || (TIME_UNIT = {})); - // ----------------------------------------------------------------------------- - // DATE - // ----------------------------------------------------------------------------- - const DATE = { - description: _t("Converts year/month/day into a date."), - args: [ - arg("year (number)", _t("The year component of the date.")), - arg("month (number)", _t("The month component of the date.")), - arg("day (number)", _t("The day component of the date.")), - ], - returns: ["DATE"], - computeFormat: function () { - return this.locale.dateFormat; - }, - compute: function (year, month, day) { - let _year = Math.trunc(toNumber(year, this.locale)); - const _month = Math.trunc(toNumber(month, this.locale)); - const _day = Math.trunc(toNumber(day, this.locale)); - // For years less than 0 or greater than 10000, return #ERROR. - assert(() => 0 <= _year && _year <= 9999, _t("The year (%s) must be between 0 and 9999 inclusive.", _year.toString())); - // Between 0 and 1899, we add that value to 1900 to calculate the year - if (_year < 1900) { - _year += 1900; - } - const jsDate = new Date(_year, _month - 1, _day); - const result = jsDateToRoundNumber(jsDate); - assert(() => result >= 0, _t(`The function [[FUNCTION_NAME]] result must be greater than or equal 01/01/1900.`)); - return result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DATEDIF - // ----------------------------------------------------------------------------- - const DATEDIF = { - description: _t("Calculates the number of days, months, or years between two dates."), - args: [ - arg("start_date (date)", _t("The start date to consider in the calculation. Must be a reference to a cell containing a DATE, a function returning a DATE type, or a number.")), - arg("end_date (date)", _t("The end date to consider in the calculation. Must be a reference to a cell containing a DATE, a function returning a DATE type, or a number.")), - arg("unit (string)", _t(`A text abbreviation for unit of time. Accepted values are "Y" (the number of whole years between start_date and end_date), "M" (the number of whole months between start_date and end_date), "D" (the number of days between start_date and end_date), "MD" (the number of days between start_date and end_date after subtracting whole months), "YM" (the number of whole months between start_date and end_date after subtracting whole years), "YD" (the number of days between start_date and end_date, assuming start_date and end_date were no more than one year apart).`)), - ], - returns: ["NUMBER"], - compute: function (startDate, endDate, unit) { - const _unit = toString(unit).toUpperCase(); - assert(() => Object.values(TIME_UNIT).includes(_unit), expectStringSetError(Object.values(TIME_UNIT), toString(unit))); - const _startDate = Math.trunc(toNumber(startDate, this.locale)); - const _endDate = Math.trunc(toNumber(endDate, this.locale)); - const jsStartDate = numberToJsDate(_startDate); - const jsEndDate = numberToJsDate(_endDate); - assert(() => _endDate >= _startDate, _t("start_date (%s) should be on or before end_date (%s).", jsStartDate.toLocaleDateString(), jsEndDate.toLocaleDateString())); - switch (_unit) { - case TIME_UNIT.WHOLE_YEARS: - return getTimeDifferenceInWholeYears(jsStartDate, jsEndDate); - case TIME_UNIT.WHOLE_MONTHS: - return getTimeDifferenceInWholeMonths(jsStartDate, jsEndDate); - case TIME_UNIT.WHOLE_DAYS: { - return getTimeDifferenceInWholeDays(jsStartDate, jsEndDate); - } - case TIME_UNIT.MONTH_WITHOUT_WHOLE_YEARS: { - return (getTimeDifferenceInWholeMonths(jsStartDate, jsEndDate) - - getTimeDifferenceInWholeYears(jsStartDate, jsEndDate) * 12); - } - case TIME_UNIT.DAYS_WITHOUT_WHOLE_MONTHS: - // Using "MD" may get incorrect result in Excel - // See: https://support.microsoft.com/en-us/office/datedif-function-25dba1a4-2812-480b-84dd-8b32a451b35c - let days = jsEndDate.getDate() - jsStartDate.getDate(); - if (days < 0) { - const monthBeforeEndMonth = new Date(jsEndDate.getFullYear(), jsEndDate.getMonth() - 1, 1); - const daysInMonthBeforeEndMonth = getDaysInMonth(monthBeforeEndMonth); - days = daysInMonthBeforeEndMonth - Math.abs(days); - } - return days; - case TIME_UNIT.DAYS_BETWEEN_NO_MORE_THAN_ONE_YEAR: { - if (areTwoDatesWithinOneYear(_startDate, _endDate)) { - return getTimeDifferenceInWholeDays(jsStartDate, jsEndDate); - } - const endDateWithinOneYear = new Date(jsStartDate.getFullYear(), jsEndDate.getMonth(), jsEndDate.getDate()); - let days = getTimeDifferenceInWholeDays(jsStartDate, endDateWithinOneYear); - if (days < 0) { - endDateWithinOneYear.setFullYear(jsStartDate.getFullYear() + 1); - days = getTimeDifferenceInWholeDays(jsStartDate, endDateWithinOneYear); - } - return days; - } - } - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DATEVALUE - // ----------------------------------------------------------------------------- - const DATEVALUE = { - description: _t("Converts a date string to a date value."), - args: [arg("date_string (string)", _t("The string representing the date."))], - returns: ["NUMBER"], - compute: function (dateString) { - const _dateString = toString(dateString); - const internalDate = parseDateTime(_dateString, this.locale); - assert(() => internalDate !== null, _t("The date_string (%s) cannot be parsed to date/time.", _dateString.toString())); - return Math.trunc(internalDate.value); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DAY - // ----------------------------------------------------------------------------- - const DAY = { - description: _t("Day of the month that a specific date falls on."), - args: [arg("date (string)", _t("The date from which to extract the day."))], - returns: ["NUMBER"], - compute: function (date) { - return toJsDate(date, this.locale).getDate(); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DAYS - // ----------------------------------------------------------------------------- - const DAYS = { - description: _t("Number of days between two dates."), - args: [ - arg("end_date (date)", _t("The end of the date range.")), - arg("start_date (date)", _t("The start of the date range.")), - ], - returns: ["NUMBER"], - compute: function (endDate, startDate) { - const _endDate = toJsDate(endDate, this.locale); - const _startDate = toJsDate(startDate, this.locale); - const dateDif = _endDate.getTime() - _startDate.getTime(); - return Math.round(dateDif / MS_PER_DAY); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DAYS360 - // ----------------------------------------------------------------------------- - const DEFAULT_DAY_COUNT_METHOD = 0; - const DAYS360 = { - description: _t("Number of days between two dates on a 360-day year (months of 30 days)."), - args: [ - arg("start_date (date)", _t("The start date to consider in the calculation.")), - arg("end_date (date)", _t("The end date to consider in the calculation.")), - arg(`method (number, default=${DEFAULT_DAY_COUNT_METHOD})`, _t("An indicator of what day count method to use. (0) US NASD method (1) European method")), - ], - returns: ["NUMBER"], - compute: function (startDate, endDate, method = DEFAULT_DAY_COUNT_METHOD) { - const _startDate = toNumber(startDate, this.locale); - const _endDate = toNumber(endDate, this.locale); - const dayCountConvention = toBoolean(method) ? 4 : 0; - const yearFrac = YEARFRAC.compute.bind(this)(startDate, endDate, dayCountConvention); - return Math.sign(_endDate - _startDate) * Math.round(yearFrac * 360); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // EDATE - // ----------------------------------------------------------------------------- - const EDATE = { - description: _t("Date a number of months before/after another date."), - args: [ - arg("start_date (date)", _t("The date from which to calculate the result.")), - arg("months (number)", _t("The number of months before (negative) or after (positive) 'start_date' to calculate.")), - ], - returns: ["DATE"], - computeFormat: function () { - return this.locale.dateFormat; - }, - compute: function (startDate, months) { - const _startDate = toJsDate(startDate, this.locale); - const _months = Math.trunc(toNumber(months, this.locale)); - const jsDate = addMonthsToDate(_startDate, _months, false); - return jsDateToRoundNumber(jsDate); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // EOMONTH - // ----------------------------------------------------------------------------- - const EOMONTH = { - description: _t("Last day of a month before or after a date."), - args: [ - arg("start_date (date)", _t("The date from which to calculate the result.")), - arg("months (number)", _t("The number of months before (negative) or after (positive) 'start_date' to consider.")), - ], - returns: ["DATE"], - computeFormat: function () { - return this.locale.dateFormat; - }, - compute: function (startDate, months) { - const _startDate = toJsDate(startDate, this.locale); - const _months = Math.trunc(toNumber(months, this.locale)); - const yStart = _startDate.getFullYear(); - const mStart = _startDate.getMonth(); - const jsDate = new Date(yStart, mStart + _months + 1, 0); - return jsDateToRoundNumber(jsDate); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // HOUR - // ----------------------------------------------------------------------------- - const HOUR = { - description: _t("Hour component of a specific time."), - args: [arg("time (date)", _t("The time from which to calculate the hour component."))], - returns: ["NUMBER"], - compute: function (date) { - return toJsDate(date, this.locale).getHours(); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ISOWEEKNUM - // ----------------------------------------------------------------------------- - const ISOWEEKNUM = { - description: _t("ISO week number of the year."), - args: [ - arg("date (date)", _t("The date for which to determine the ISO week number. Must be a reference to a cell containing a date, a function returning a date type, or a number.")), - ], - returns: ["NUMBER"], - compute: function (date) { - const _date = toJsDate(date, this.locale); - const y = _date.getFullYear(); - // 1 - As the 1st week of a year can start the previous year or after the 1st - // january we first look if the date is in the weeks of the current year, previous - // year or year after. - // A - We look for the current year, the first days of the first week - // and the last days of the last week - // The first week of the year is the week that contains the first - // Thursday of the year. - let firstThursday = 1; - while (new Date(y, 0, firstThursday).getDay() !== 4) { - firstThursday += 1; - } - const firstDayOfFirstWeek = new Date(y, 0, firstThursday - 3); - // The last week of the year is the week that contains the last Thursday of - // the year. - let lastThursday = 31; - while (new Date(y, 11, lastThursday).getDay() !== 4) { - lastThursday -= 1; - } - const lastDayOfLastWeek = new Date(y, 11, lastThursday + 3); - // B - If our date > lastDayOfLastWeek then it's in the weeks of the year after - // If our date < firstDayOfFirstWeek then it's in the weeks of the year before - let offsetYear; - if (firstDayOfFirstWeek.getTime() <= _date.getTime()) { - if (_date.getTime() <= lastDayOfLastWeek.getTime()) { - offsetYear = 0; - } - else { - offsetYear = 1; - } - } - else { - offsetYear = -1; - } - // 2 - now that the year is known, we are looking at the difference between - // the first day of this year and the date. The difference in days divided by - // 7 gives us the week number - let firstDay; - switch (offsetYear) { - case 0: - firstDay = firstDayOfFirstWeek; - break; - case 1: - // firstDay is the 1st day of the 1st week of the year after - // firstDay = lastDayOfLastWeek + 1 Day - firstDay = new Date(y, 11, lastThursday + 3 + 1); - break; - case -1: - // firstDay is the 1st day of the 1st week of the previous year. - // The first week of the previous year is the week that contains the - // first Thursday of the previous year. - let firstThursdayPreviousYear = 1; - while (new Date(y - 1, 0, firstThursdayPreviousYear).getDay() !== 4) { - firstThursdayPreviousYear += 1; - } - firstDay = new Date(y - 1, 0, firstThursdayPreviousYear - 3); - break; - } - const diff = (_date.getTime() - firstDay.getTime()) / MS_PER_DAY; - return Math.floor(diff / 7) + 1; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MINUTE - // ----------------------------------------------------------------------------- - const MINUTE = { - description: _t("Minute component of a specific time."), - args: [arg("time (date)", _t("The time from which to calculate the minute component."))], - returns: ["NUMBER"], - compute: function (date) { - return toJsDate(date, this.locale).getMinutes(); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MONTH - // ----------------------------------------------------------------------------- - const MONTH = { - description: _t("Month of the year a specific date falls in"), - args: [arg("date (date)", _t("The date from which to extract the month."))], - returns: ["NUMBER"], - compute: function (date) { - return toJsDate(date, this.locale).getMonth() + 1; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // NETWORKDAYS - // ----------------------------------------------------------------------------- - const NETWORKDAYS = { - description: _t("Net working days between two provided days."), - args: [ - arg("start_date (date)", _t("The start date of the period from which to calculate the number of net working days.")), - arg("end_date (date)", _t("The end date of the period from which to calculate the number of net working days.")), - arg("holidays (date, range, optional)", _t("A range or array constant containing the date serial numbers to consider holidays.")), - ], - returns: ["NUMBER"], - compute: function (startDate, endDate, holidays) { - return NETWORKDAYS_INTL.compute.bind(this)(startDate, endDate, 1, holidays); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // NETWORKDAYS.INTL - // ----------------------------------------------------------------------------- - /** - * Transform weekend Spreadsheet information into Date Day JavaScript information. - * Take string (String method) or number (Number method), return array of numbers. - * - * String method: weekends can be specified using seven 0’s and 1’s, where the - * first number in the set represents Monday and the last number is for Sunday. - * A zero means that the day is a work day, a 1 means that the day is a weekend. - * For example, “0000011” would mean Saturday and Sunday are weekends. - * - * Number method: instead of using the string method above, a single number can - * be used. 1 = Saturday/Sunday are weekends, 2 = Sunday/Monday, and this pattern - * repeats until 7 = Friday/Saturday. 11 = Sunday is the only weekend, 12 = Monday - * is the only weekend, and this pattern repeats until 17 = Saturday is the only - * weekend. - * - * Example: - * - 11 return [0] (correspond to Sunday) - * - 12 return [1] (correspond to Monday) - * - 3 return [1,2] (correspond to Monday and Tuesday) - * - "0101010" return [2,4,6] (correspond to Tuesday, Thursday and Saturday) - */ - function weekendToDayNumber(weekend) { - // case "string" - if (typeof weekend === "string") { - assert(() => { - if (weekend.length !== 7) { - return false; - } - for (let day of weekend) { - if (day !== "0" && day !== "1") { - return false; - } - } - return true; - }, _t('When weekend is a string (%s) it must be composed of "0" or "1".', weekend)); - let result = []; - for (let i = 0; i < 7; i++) { - if (weekend[i] === "1") { - result.push((i + 1) % 7); - } - } - return result; - } - //case "number" - if (typeof weekend === "number") { - assert(() => (1 <= weekend && weekend <= 7) || (11 <= weekend && weekend <= 17), _t("The weekend (%s) must be a string or a number in the range 1-7 or 11-17.", weekend.toString())); - // case 1 <= weekend <= 7 - if (weekend <= 7) { - // 1 = Saturday/Sunday are weekends - // 2 = Sunday/Monday - // ... - // 7 = Friday/Saturday. - return [weekend - 2 === -1 ? 6 : weekend - 2, weekend - 1]; - } - // case 11 <= weekend <= 17 - // 11 = Sunday is the only weekend - // 12 = Monday is the only weekend - // ... - // 17 = Saturday is the only weekend. - return [weekend - 11]; - } - throw Error(_t("The weekend must be a number or a string.")); - } - const NETWORKDAYS_INTL = { - description: _t("Net working days between two dates (specifying weekends)."), - args: [ - arg("start_date (date)", _t("The start date of the period from which to calculate the number of net working days.")), - arg("end_date (date)", _t("The end date of the period from which to calculate the number of net working days.")), - arg(`weekend (any, default=${DEFAULT_WEEKEND})`, _t("A number or string representing which days of the week are considered weekends.")), - arg("holidays (date, range, optional)", _t("A range or array constant containing the dates to consider as holidays.")), - ], - returns: ["NUMBER"], - compute: function (startDate, endDate, weekend = DEFAULT_WEEKEND, holidays) { - const _startDate = toJsDate(startDate, this.locale); - const _endDate = toJsDate(endDate, this.locale); - const daysWeekend = weekendToDayNumber(weekend); - let timesHoliday = new Set(); - if (holidays !== undefined) { - visitAny([holidays], (h) => { - const holiday = toJsDate(h, this.locale); - timesHoliday.add(holiday.getTime()); - }); - } - const invertDate = _startDate.getTime() > _endDate.getTime(); - const stopDate = new Date((invertDate ? _startDate : _endDate).getTime()); - let stepDate = new Date((invertDate ? _endDate : _startDate).getTime()); - const timeStopDate = stopDate.getTime(); - let timeStepDate = stepDate.getTime(); - let netWorkingDay = 0; - while (timeStepDate <= timeStopDate) { - if (!daysWeekend.includes(stepDate.getDay()) && !timesHoliday.has(timeStepDate)) { - netWorkingDay += 1; - } - stepDate.setDate(stepDate.getDate() + 1); - timeStepDate = stepDate.getTime(); - } - return invertDate ? -netWorkingDay : netWorkingDay; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // NOW - // ----------------------------------------------------------------------------- - const NOW = { - description: _t("Current date and time as a date value."), - args: [], - returns: ["DATE"], - computeFormat: function () { - return this.locale.dateFormat + " " + this.locale.timeFormat; - }, - compute: function () { - let today = new Date(); - today.setMilliseconds(0); - const delta = today.getTime() - INITIAL_1900_DAY.getTime(); - const time = today.getHours() / 24 + today.getMinutes() / 1440 + today.getSeconds() / 86400; - return Math.floor(delta / MS_PER_DAY) + time; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SECOND - // ----------------------------------------------------------------------------- - const SECOND = { - description: _t("Minute component of a specific time."), - args: [arg("time (date)", _t("The time from which to calculate the second component."))], - returns: ["NUMBER"], - compute: function (date) { - return toJsDate(date, this.locale).getSeconds(); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TIME - // ----------------------------------------------------------------------------- - const TIME = { - description: _t("Converts hour/minute/second into a time."), - args: [ - arg("hour (number)", _t("The hour component of the time.")), - arg("minute (number)", _t("The minute component of the time.")), - arg("second (number)", _t("The second component of the time.")), - ], - returns: ["DATE"], - computeFormat: function () { - return this.locale.timeFormat; - }, - compute: function (hour, minute, second) { - let _hour = Math.trunc(toNumber(hour, this.locale)); - let _minute = Math.trunc(toNumber(minute, this.locale)); - let _second = Math.trunc(toNumber(second, this.locale)); - _minute += Math.floor(_second / 60); - _second = (_second % 60) + (_second < 0 ? 60 : 0); - _hour += Math.floor(_minute / 60); - _minute = (_minute % 60) + (_minute < 0 ? 60 : 0); - _hour %= 24; - assert(() => _hour >= 0, _t(`The function [[FUNCTION_NAME]] result cannot be negative`)); - return _hour / 24 + _minute / (24 * 60) + _second / (24 * 60 * 60); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TIMEVALUE - // ----------------------------------------------------------------------------- - const TIMEVALUE = { - description: _t("Converts a time string into its serial number representation."), - args: [arg("time_string (string)", _t("The string that holds the time representation."))], - returns: ["NUMBER"], - compute: function (timeString) { - const _timeString = toString(timeString); - const internalDate = parseDateTime(_timeString, this.locale); - assert(() => internalDate !== null, _t("The time_string (%s) cannot be parsed to date/time.", _timeString)); - const result = internalDate.value - Math.trunc(internalDate.value); - return result < 0 ? 1 + result : result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TODAY - // ----------------------------------------------------------------------------- - const TODAY = { - description: _t("Current date as a date value."), - args: [], - returns: ["DATE"], - computeFormat: function () { - return this.locale.dateFormat; - }, - compute: function () { - const today = new Date(); - const jsDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()); - return jsDateToRoundNumber(jsDate); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // WEEKDAY - // ----------------------------------------------------------------------------- - const WEEKDAY = { - description: _t("Day of the week of the date provided (as number)."), - args: [ - arg("date (date)", _t("The date for which to determine the day of the week. Must be a reference to a cell containing a date, a function returning a date type, or a number.")), - arg(`type (number, default=${DEFAULT_TYPE})`, _t("A number indicating which numbering system to use to represent weekdays. By default, counts starting with Sunday = 1.")), - ], - returns: ["NUMBER"], - compute: function (date, type = DEFAULT_TYPE) { - const _date = toJsDate(date, this.locale); - const _type = Math.round(toNumber(type, this.locale)); - const m = _date.getDay(); - assert(() => [1, 2, 3].includes(_type), _t("The type (%s) must be 1, 2 or 3.", _type.toString())); - if (_type === 1) - return m + 1; - if (_type === 2) - return m === 0 ? 7 : m; - return m === 0 ? 6 : m - 1; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // WEEKNUM - // ----------------------------------------------------------------------------- - const WEEKNUM = { - description: _t("Week number of the year."), - args: [ - arg("date (date)", _t("The date for which to determine the week number. Must be a reference to a cell containing a date, a function returning a date type, or a number.")), - arg(`type (number, default=${DEFAULT_TYPE})`, _t("A number representing the day that a week starts on. Sunday = 1.")), - ], - returns: ["NUMBER"], - compute: function (date, type = DEFAULT_TYPE) { - const _date = toJsDate(date, this.locale); - const _type = Math.round(toNumber(type, this.locale)); - assert(() => _type === 1 || _type === 2 || (11 <= _type && _type <= 17) || _type === 21, _t("The type (%s) is out of range.", _type.toString())); - if (_type === 21) { - return ISOWEEKNUM.compute.bind(this)(date); - } - let startDayOfWeek; - if (_type === 1 || _type === 2) { - startDayOfWeek = _type - 1; - } - else { - // case 11 <= _type <= 17 - startDayOfWeek = _type - 10 === 7 ? 0 : _type - 10; - } - const y = _date.getFullYear(); - let dayStart = 1; - let startDayOfFirstWeek = new Date(y, 0, dayStart); - while (startDayOfFirstWeek.getDay() !== startDayOfWeek) { - dayStart += 1; - startDayOfFirstWeek = new Date(y, 0, dayStart); - } - const dif = (_date.getTime() - startDayOfFirstWeek.getTime()) / MS_PER_DAY; - if (dif < 0) { - return 1; - } - return Math.floor(dif / 7) + (dayStart === 1 ? 1 : 2); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // WORKDAY - // ----------------------------------------------------------------------------- - const WORKDAY = { - description: _t("Date after a number of workdays."), - args: [ - arg("start_date (date)", _t("The date from which to begin counting.")), - arg("num_days (number)", _t("The number of working days to advance from start_date. If negative, counts backwards.")), - arg("holidays (date, range, optional)", _t("A range or array constant containing the dates to consider holidays.")), - ], - returns: ["NUMBER"], - computeFormat: function () { - return this.locale.dateFormat; - }, - compute: function (startDate, numDays, holidays = undefined) { - return WORKDAY_INTL.compute.bind(this)(startDate, numDays, 1, holidays ?? null); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // WORKDAY.INTL - // ----------------------------------------------------------------------------- - const WORKDAY_INTL = { - description: _t("Date after a number of workdays (specifying weekends)."), - args: [ - arg("start_date (date)", _t("The date from which to begin counting.")), - arg("num_days (number)", _t("The number of working days to advance from start_date. If negative, counts backwards.")), - arg(`weekend (any, default=${DEFAULT_WEEKEND})`, _t("A number or string representing which days of the week are considered weekends.")), - arg("holidays (date, range, optional)", _t("A range or array constant containing the dates to consider holidays.")), - ], - returns: ["DATE"], - computeFormat: function () { - return this.locale.dateFormat; - }, - compute: function (startDate, numDays, weekend = DEFAULT_WEEKEND, holidays) { - let _startDate = toJsDate(startDate, this.locale); - let _numDays = Math.trunc(toNumber(numDays, this.locale)); - if (typeof weekend === "string") { - assert(() => weekend !== "1111111", _t("The weekend (%s) must be different from '1111111'.", weekend)); - } - const daysWeekend = weekendToDayNumber(weekend); - let timesHoliday = new Set(); - if (holidays !== undefined) { - visitAny([holidays], (h) => { - const holiday = toJsDate(h, this.locale); - timesHoliday.add(holiday.getTime()); - }); - } - let stepDate = new Date(_startDate.getTime()); - let timeStepDate = stepDate.getTime(); - const unitDay = Math.sign(_numDays); - let stepDay = Math.abs(_numDays); - while (stepDay > 0) { - stepDate.setDate(stepDate.getDate() + unitDay); - timeStepDate = stepDate.getTime(); - if (!daysWeekend.includes(stepDate.getDay()) && !timesHoliday.has(timeStepDate)) { - stepDay -= 1; - } - } - const delta = timeStepDate - INITIAL_1900_DAY.getTime(); - return Math.round(delta / MS_PER_DAY); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // YEAR - // ----------------------------------------------------------------------------- - const YEAR = { - description: _t("Year specified by a given date."), - args: [arg("date (date)", _t("The date from which to extract the year."))], - returns: ["NUMBER"], - compute: function (date) { - return toJsDate(date, this.locale).getFullYear(); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // YEARFRAC - // ----------------------------------------------------------------------------- - const DEFAULT_DAY_COUNT_CONVENTION$1 = 0; - const YEARFRAC = { - description: _t("Exact number of years between two dates."), - args: [ - arg("start_date (date)", _t("The start date to consider in the calculation. Must be a reference to a cell containing a date, a function returning a date type, or a number.")), - arg("end_date (date)", _t("The end date to consider in the calculation. Must be a reference to a cell containing a date, a function returning a date type, or a number.")), - arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION$1})`, _t("An indicator of what day count method to use.")), - ], - returns: ["NUMBER"], - compute: function (startDate, endDate, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION$1) { - let _startDate = Math.trunc(toNumber(startDate, this.locale)); - let _endDate = Math.trunc(toNumber(endDate, this.locale)); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assert(() => _startDate >= 0, _t("The start_date (%s) must be positive or null.", _startDate.toString())); - assert(() => _endDate >= 0, _t("The end_date (%s) must be positive or null.", _endDate.toString())); - assert(() => 0 <= _dayCountConvention && _dayCountConvention <= 4, _t("The day_count_convention (%s) must be between 0 and 4 inclusive.", _dayCountConvention.toString())); - return getYearFrac(_startDate, _endDate, _dayCountConvention); - }, - }; - // ----------------------------------------------------------------------------- - // MONTH.START - // ----------------------------------------------------------------------------- - const MONTH_START = { - description: _t("First day of the month preceding a date."), - args: [arg("date (date)", _t("The date from which to calculate the result."))], - returns: ["DATE"], - computeFormat: function () { - return this.locale.dateFormat; - }, - compute: function (date) { - const _startDate = toJsDate(date, this.locale); - const yStart = _startDate.getFullYear(); - const mStart = _startDate.getMonth(); - const jsDate = new Date(yStart, mStart, 1); - return jsDateToRoundNumber(jsDate); - }, - }; - // ----------------------------------------------------------------------------- - // MONTH.END - // ----------------------------------------------------------------------------- - const MONTH_END = { - description: _t("Last day of the month following a date."), - args: [arg("date (date)", _t("The date from which to calculate the result."))], - returns: ["DATE"], - computeFormat: function () { - return this.locale.dateFormat; - }, - compute: function (date) { - return EOMONTH.compute.bind(this)(date, 0); - }, - }; - // ----------------------------------------------------------------------------- - // QUARTER - // ----------------------------------------------------------------------------- - const QUARTER = { - description: _t("Quarter of the year a specific date falls in"), - args: [arg("date (date)", _t("The date from which to extract the quarter."))], - returns: ["NUMBER"], - compute: function (date) { - return Math.ceil((toJsDate(date, this.locale).getMonth() + 1) / 3); - }, - }; - // ----------------------------------------------------------------------------- - // QUARTER.START - // ----------------------------------------------------------------------------- - const QUARTER_START = { - description: _t("First day of the quarter of the year a specific date falls in."), - args: [arg("date (date)", _t("The date from which to calculate the start of quarter."))], - returns: ["DATE"], - computeFormat: function () { - return this.locale.dateFormat; - }, - compute: function (date) { - const quarter = QUARTER.compute.bind(this)(date); - const year = YEAR.compute.bind(this)(date); - const jsDate = new Date(year, (quarter - 1) * 3, 1); - return jsDateToRoundNumber(jsDate); - }, - }; - // ----------------------------------------------------------------------------- - // QUARTER.END - // ----------------------------------------------------------------------------- - const QUARTER_END = { - description: _t("Last day of the quarter of the year a specific date falls in."), - args: [arg("date (date)", _t("The date from which to calculate the end of quarter."))], - returns: ["DATE"], - computeFormat: function () { - return this.locale.dateFormat; - }, - compute: function (date) { - const quarter = QUARTER.compute.bind(this)(date); - const year = YEAR.compute.bind(this)(date); - const jsDate = new Date(year, quarter * 3, 0); - return jsDateToRoundNumber(jsDate); - }, - }; - // ----------------------------------------------------------------------------- - // YEAR.START - // ----------------------------------------------------------------------------- - const YEAR_START = { - description: _t("First day of the year a specific date falls in."), - args: [arg("date (date)", _t("The date from which to calculate the start of the year."))], - returns: ["DATE"], - computeFormat: function () { - return this.locale.dateFormat; - }, - compute: function (date) { - const year = YEAR.compute.bind(this)(date); - const jsDate = new Date(year, 0, 1); - return jsDateToRoundNumber(jsDate); - }, - }; - // ----------------------------------------------------------------------------- - // YEAR.END - // ----------------------------------------------------------------------------- - const YEAR_END = { - description: _t("Last day of the year a specific date falls in."), - args: [arg("date (date)", _t("The date from which to calculate the end of the year."))], - returns: ["DATE"], - computeFormat: function () { - return this.locale.dateFormat; - }, - compute: function (date) { - const year = YEAR.compute.bind(this)(date); - const jsDate = new Date(year + 1, 0, 0); - return jsDateToRoundNumber(jsDate); - }, - }; - - var date = /*#__PURE__*/Object.freeze({ - __proto__: null, - DATE: DATE, - DATEDIF: DATEDIF, - DATEVALUE: DATEVALUE, - DAY: DAY, - DAYS: DAYS, - DAYS360: DAYS360, - EDATE: EDATE, - EOMONTH: EOMONTH, - HOUR: HOUR, - ISOWEEKNUM: ISOWEEKNUM, - MINUTE: MINUTE, - MONTH: MONTH, - MONTH_END: MONTH_END, - MONTH_START: MONTH_START, - NETWORKDAYS: NETWORKDAYS, - NETWORKDAYS_INTL: NETWORKDAYS_INTL, - NOW: NOW, - QUARTER: QUARTER, - QUARTER_END: QUARTER_END, - QUARTER_START: QUARTER_START, - SECOND: SECOND, - TIME: TIME, - TIMEVALUE: TIMEVALUE, - TODAY: TODAY, - WEEKDAY: WEEKDAY, - WEEKNUM: WEEKNUM, - WORKDAY: WORKDAY, - WORKDAY_INTL: WORKDAY_INTL, - YEAR: YEAR, - YEARFRAC: YEARFRAC, - YEAR_END: YEAR_END, - YEAR_START: YEAR_START - }); - - const DEFAULT_DELTA_ARG = 0; - // ----------------------------------------------------------------------------- - // DELTA - // ----------------------------------------------------------------------------- - const DELTA = { - description: _t("Compare two numeric values, returning 1 if they're equal."), - args: [ - arg(" (number)", _t("The first number to compare.")), - arg(` (number, default=${DEFAULT_DELTA_ARG})`, _t("The second number to compare.")), - ], - returns: ["NUMBER"], - compute: function (number1, number2 = DEFAULT_DELTA_ARG) { - const _number1 = toNumber(number1, this.locale); - const _number2 = toNumber(number2, this.locale); - return _number1 === _number2 ? 1 : 0; - }, - isExported: true, - }; - - var engineering = /*#__PURE__*/Object.freeze({ - __proto__: null, - DELTA: DELTA - }); - - // ----------------------------------------------------------------------------- - // FILTER - // ----------------------------------------------------------------------------- - const FILTER = { - description: _t("Returns a filtered version of the source range, returning only rows or columns that meet the specified conditions."), - // TODO modify args description when vectorization on formulas is available - args: [ - arg("range (any, range)", _t("The data to be filtered.")), - arg("condition1 (boolean, range)", _t("A column or row containing true or false values corresponding to the first column or row of range.")), - arg("condition2 (boolean, range, repeating)", _t("Additional column or row containing true or false values.")), - ], - returns: ["RANGE"], - computeValueAndFormat: function (range, ...conditions) { - let _array = toMatrix(range); - const _conditionsMatrices = conditions.map((cond) => matrixMap(toMatrix(cond), (data) => data.value)); - _conditionsMatrices.map((c) => assertSingleColOrRow(_t("The arguments condition must be a single column or row."), c)); - assertSameDimensions(_t("The arguments conditions must have the same dimensions."), ..._conditionsMatrices); - const _conditions = _conditionsMatrices.map((c) => c.flat()); - const mode = _conditionsMatrices[0].length === 1 ? "row" : "col"; - _array = mode === "row" ? transposeMatrix(_array) : _array; - assert(() => _conditions.every((cond) => cond.length === _array.length), _t(`FILTER has mismatched sizes on the range and conditions.`)); - const result = []; - for (let i = 0; i < _array.length; i++) { - const row = _array[i]; - if (_conditions.every((c) => c[i])) { - result.push(row); - } - } - if (!result.length) { - throw new NotAvailableError(_t("No match found in FILTER evaluation")); - } - return mode === "row" ? transposeMatrix(result) : result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // UNIQUE - // ----------------------------------------------------------------------------- - const UNIQUE = { - description: _t("Unique rows in the provided source range."), - args: [ - arg("range (any, range)", _t("The data to filter by unique entries.")), - arg("by_column (boolean, default=FALSE)", _t("Whether to filter the data by columns or by rows.")), - arg("exactly_once (boolean, default=FALSE)", _t("Whether to return only entries with no duplicates.")), - ], - returns: ["RANGE"], - computeValueAndFormat: function (range = { value: "" }, byColumn, exactlyOnce) { - if (!isMatrix(range)) { - return [[range]]; - } - const _byColumn = toBoolean(byColumn?.value) || false; - const _exactlyOnce = toBoolean(exactlyOnce?.value) || false; - if (!_byColumn) { - range = transposeMatrix(range); - } - const map = new Map(); - for (const data of range) { - const key = JSON.stringify(data.map((item) => item.value)); - const occurrence = map.get(key); - if (!occurrence) { - map.set(key, { data, count: 1 }); - } - else { - occurrence.count++; - } - } - const result = []; - for (const row of map.values()) { - if (_exactlyOnce && row.count > 1) { - continue; - } - result.push(row.data); - } - if (!result.length) - throw new Error(_t("No unique values found")); - return _byColumn ? result : transposeMatrix(result); - }, - isExported: true, - }; - - var filter = /*#__PURE__*/Object.freeze({ - __proto__: null, - FILTER: FILTER, - UNIQUE: UNIQUE - }); - - /** Assert maturity date > settlement date */ - function assertMaturityAndSettlementDatesAreValid(settlement, maturity) { - assert(() => settlement < maturity, _t("The maturity (%s) must be strictly greater than the settlement (%s).", maturity.toString(), settlement.toString())); - } - /** Assert settlement date > issue date */ - function assertSettlementAndIssueDatesAreValid(settlement, issue) { - assert(() => issue < settlement, _t("The settlement date (%s) must be strictly greater than the issue date (%s).", settlement.toString(), issue.toString())); - } - /** Assert coupon frequency is in [1, 2, 4] */ - function assertCouponFrequencyIsValid(frequency) { - assert(() => [1, 2, 4].includes(frequency), _t("The frequency (%s) must be one of %s", frequency.toString(), [1, 2, 4].toString())); - } - /** Assert dayCountConvention is between 0 and 4 */ - function assertDayCountConventionIsValid(dayCountConvention) { - assert(() => 0 <= dayCountConvention && dayCountConvention <= 4, _t("The day_count_convention (%s) must be between 0 and 4 inclusive.", dayCountConvention.toString())); - } - function assertRedemptionStrictlyPositive(redemption) { - assert(() => redemption > 0, _t("The redemption (%s) must be strictly positive.", redemption.toString())); - } - function assertPriceStrictlyPositive(price) { - assert(() => price > 0, _t("The price (%s) must be strictly positive.", price.toString())); - } - function assertNumberOfPeriodsStrictlyPositive(nPeriods) { - assert(() => nPeriods > 0, _t("The number_of_periods (%s) must be greater than 0.", nPeriods.toString())); - } - function assertRateStrictlyPositive(rate) { - assert(() => rate > 0, _t("The rate (%s) must be strictly positive.", rate.toString())); - } - function assertLifeStrictlyPositive(life) { - assert(() => life > 0, _t("The life (%s) must be strictly positive.", life.toString())); - } - function assertCostStrictlyPositive(cost) { - assert(() => cost > 0, _t("The cost (%s) must be strictly positive.", cost.toString())); - } - function assertCostPositiveOrZero(cost) { - assert(() => cost >= 0, _t("The cost (%s) must be positive or null.", cost.toString())); - } - function assertPeriodStrictlyPositive(period) { - assert(() => period > 0, _t("The period (%s) must be strictly positive.", period.toString())); - } - function assertPeriodPositiveOrZero(period) { - assert(() => period >= 0, _t("The period (%s) must be positive or null.", period.toString())); - } - function assertSalvagePositiveOrZero(salvage) { - assert(() => salvage >= 0, _t("The salvage (%s) must be positive or null.", salvage.toString())); - } - function assertSalvageSmallerOrEqualThanCost(salvage, cost) { - assert(() => salvage <= cost, _t("The salvage (%s) must be smaller or equal than the cost (%s).", salvage.toString(), cost.toString())); - } - function assertPresentValueStrictlyPositive(pv) { - assert(() => pv > 0, _t("The present value (%s) must be strictly positive.", pv.toString())); - } - function assertPeriodSmallerOrEqualToLife(period, life) { - assert(() => period <= life, _t("The period (%s) must be less than or equal life (%s).", period.toString(), life.toString())); - } - function assertInvestmentStrictlyPositive(investment) { - assert(() => investment > 0, _t("The investment (%s) must be strictly positive.", investment.toString())); - } - function assertDiscountStrictlyPositive(discount) { - assert(() => discount > 0, _t("The discount (%s) must be strictly positive.", discount.toString())); - } - function assertDiscountStrictlySmallerThanOne(discount) { - assert(() => discount < 1, _t("The discount (%s) must be smaller than 1.", discount.toString())); - } - function assertDeprecationFactorStrictlyPositive(factor) { - assert(() => factor > 0, _t("The depreciation factor (%s) must be strictly positive.", factor.toString())); - } - function assertSettlementLessThanOneYearBeforeMaturity(settlement, maturity, locale) { - const startDate = toJsDate(settlement, locale); - const endDate = toJsDate(maturity, locale); - const startDatePlusOneYear = new Date(startDate); - startDatePlusOneYear.setFullYear(startDate.getFullYear() + 1); - assert(() => endDate.getTime() <= startDatePlusOneYear.getTime(), _t("The settlement date (%s) must at most one year after the maturity date (%s).", settlement.toString(), maturity.toString())); - } - /** - * Check if the given periods are valid. This will assert : - * - * - 0 < numberOfPeriods - * - 0 < firstPeriod <= lastPeriod - * - 0 < lastPeriod <= numberOfPeriods - * - */ - function assertFirstAndLastPeriodsAreValid(firstPeriod, lastPeriod, numberOfPeriods) { - assertNumberOfPeriodsStrictlyPositive(numberOfPeriods); - assert(() => firstPeriod > 0, _t("The first_period (%s) must be strictly positive.", firstPeriod.toString())); - assert(() => lastPeriod > 0, _t("The last_period (%s) must be strictly positive.", lastPeriod.toString())); - assert(() => firstPeriod <= lastPeriod, _t("The first_period (%s) must be smaller or equal to the last_period (%s).", firstPeriod.toString(), lastPeriod.toString())); - assert(() => lastPeriod <= numberOfPeriods, _t("The last_period (%s) must be smaller or equal to the number_of_periods (%s).", firstPeriod.toString(), numberOfPeriods.toString())); - } - /** - * Check if the given periods are valid. This will assert : - * - * - 0 < life - * - 0 <= startPeriod <= endPeriod - * - 0 <= endPeriod <= life - * - */ - function assertStartAndEndPeriodAreValid(startPeriod, endPeriod, life) { - assertLifeStrictlyPositive(life); - assert(() => startPeriod >= 0, _t("The start_period (%s) must be greater or equal than 0.", startPeriod.toString())); - assert(() => endPeriod >= 0, _t("The end_period (%s) must be greater or equal than 0.", endPeriod.toString())); - assert(() => startPeriod <= endPeriod, _t("The start_period (%s) must be smaller or equal to the end_period (%s).", startPeriod.toString(), endPeriod.toString())); - assert(() => endPeriod <= life, _t("The end_period (%s) must be smaller or equal to the life (%s).", startPeriod.toString(), life.toString())); - } - function assertRateGuessStrictlyGreaterThanMinusOne(guess) { - assert(() => guess > -1, _t("The rate_guess (%s) must be strictly greater than -1.", guess.toString())); - } - function assertCashFlowsAndDatesHaveSameDimension(cashFlows, dates) { - assert(() => cashFlows.length === dates.length && cashFlows[0].length === dates[0].length, _t("The cashflow_amounts and cashflow_dates ranges must have the same dimensions.")); - } - function assertCashFlowsHavePositiveAndNegativesValues(cashFlow) { - assert(() => cashFlow.some((val) => val > 0) && cashFlow.some((val) => val < 0), _t("There must be both positive and negative values in cashflow_amounts.")); - } - function assertEveryDateGreaterThanFirstDateOfCashFlowDates(dates) { - assert(() => dates.every((date) => date >= dates[0]), _t("All the dates should be greater or equal to the first date in cashflow_dates (%s).", dates[0].toString())); - } - - const DEFAULT_DAY_COUNT_CONVENTION = 0; - const DEFAULT_END_OR_BEGINNING = 0; - const DEFAULT_FUTURE_VALUE = 0; - const COUPON_FUNCTION_ARGS = [ - arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), - arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), - arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4).")), - arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), - ]; - /** - * Use the Newton–Raphson method to find a root of the given function in an iterative manner. - * - * @param func the function to find a root of - * @param derivFunc the derivative of the function - * @param startValue the initial value for the first iteration of the algorithm - * @param maxIterations the maximum number of iterations - * @param epsMax the epsilon for the root - * @param nanFallback a function giving a fallback value to use if func(x) returns NaN. Useful if the - * function is not defined for some range, but we know approximately where the root is when the Newton - * algorithm ends up in this range. - */ - function newtonMethod(func, derivFunc, startValue, maxIterations, epsMax = 1e-10, nanFallback) { - let x = startValue; - let newX; - let xDelta; - let y; - let yEqual0 = false; - let count = 0; - let previousFallback = undefined; - do { - y = func(x); - if (isNaN(y)) { - assert(() => count < maxIterations && nanFallback !== undefined, _t(`Function [[FUNCTION_NAME]] didn't find any result.`)); - count++; - x = nanFallback(previousFallback); - previousFallback = x; - continue; - } - newX = x - y / derivFunc(x); - xDelta = Math.abs(newX - x); - x = newX; - yEqual0 = xDelta < epsMax || Math.abs(y) < epsMax; - assert(() => count < maxIterations, _t(`Function [[FUNCTION_NAME]] didn't find any result.`)); - count++; - } while (!yEqual0); - return x; - } - // ----------------------------------------------------------------------------- - // ACCRINTM - // ----------------------------------------------------------------------------- - const ACCRINTM = { - description: _t("Accrued interest of security paying at maturity."), - args: [ - arg("issue (date)", _t("The date the security was initially issued.")), - arg("maturity (date)", _t("The maturity date of the security.")), - arg("rate (number)", _t("The annualized rate of interest.")), - arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")), - arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), - ], - returns: ["NUMBER"], - compute: function (issue, maturity, rate, redemption, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const start = Math.trunc(toNumber(issue, this.locale)); - const end = Math.trunc(toNumber(maturity, this.locale)); - const _redemption = toNumber(redemption, this.locale); - const _rate = toNumber(rate, this.locale); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertSettlementAndIssueDatesAreValid(end, start); - assertDayCountConventionIsValid(_dayCountConvention); - assertRedemptionStrictlyPositive(_redemption); - assertRateStrictlyPositive(_rate); - const yearFrac = YEARFRAC.compute.bind(this)(start, end, dayCountConvention); - return _redemption * _rate * yearFrac; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // AMORLINC - // ----------------------------------------------------------------------------- - const AMORLINC = { - description: _t("Depreciation for an accounting period."), - args: [ - arg("cost (number)", _t("The initial cost of the asset.")), - arg("purchase_date (date)", _t("The date the asset was purchased.")), - arg("first_period_end (date)", _t("The date the first period ended.")), - arg("salvage (number)", _t("The value of the asset at the end of depreciation.")), - arg("period (number)", _t("The single period within life for which to calculate depreciation.")), - arg("rate (number)", _t("The deprecation rate.")), - arg(" (number, optional)", _t("An indicator of what day count method to use.")), - ], - returns: ["NUMBER"], - compute: function (cost, purchaseDate, firstPeriodEnd, salvage, period, rate, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const _cost = toNumber(cost, this.locale); - const _purchaseDate = Math.trunc(toNumber(purchaseDate, this.locale)); - const _firstPeriodEnd = Math.trunc(toNumber(firstPeriodEnd, this.locale)); - const _salvage = toNumber(salvage, this.locale); - const _period = toNumber(period, this.locale); - const _rate = toNumber(rate, this.locale); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertCostStrictlyPositive(_cost); - assertSalvagePositiveOrZero(_salvage); - assertSalvageSmallerOrEqualThanCost(_salvage, _cost); - assertPeriodPositiveOrZero(_period); - assertRateStrictlyPositive(_rate); - assertDayCountConventionIsValid(_dayCountConvention); - assert(() => _purchaseDate <= _firstPeriodEnd, _t("The purchase_date (%s) must be before the first_period_end (%s).", _purchaseDate.toString(), _firstPeriodEnd.toString())); - /** - * https://wiki.documentfoundation.org/Documentation/Calc_Functions/AMORLINC - * - * AMORLINC period 0 = cost * rate * YEARFRAC(purchase date, first period end) - * AMORLINC period n = cost * rate - * AMORLINC at the last period is such that the remaining deprecated cost is equal to the salvage value. - * - * The period is and rounded to 1 if < 1 truncated if > 1, - * - * Compatibility note : - * If (purchase date) === (first period end), on GSheet the deprecation at the first period is 0, and on Excel - * it is a full period deprecation. We choose to use the Excel behaviour. - */ - const roundedPeriod = _period < 1 && _period > 0 ? 1 : Math.trunc(_period); - const deprec = _cost * _rate; - const yearFrac = YEARFRAC.compute.bind(this)(_purchaseDate, _firstPeriodEnd, _dayCountConvention); - const firstDeprec = _purchaseDate === _firstPeriodEnd ? deprec : deprec * yearFrac; - const valueAtPeriod = _cost - firstDeprec - deprec * roundedPeriod; - if (valueAtPeriod >= _salvage) { - return roundedPeriod === 0 ? firstDeprec : deprec; - } - return _salvage - valueAtPeriod < deprec ? deprec - (_salvage - valueAtPeriod) : 0; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COUPDAYS - // ----------------------------------------------------------------------------- - const COUPDAYS = { - description: _t("Days in coupon period containing settlement date."), - args: COUPON_FUNCTION_ARGS, - returns: ["NUMBER"], - compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const start = Math.trunc(toNumber(settlement, this.locale)); - const end = Math.trunc(toNumber(maturity, this.locale)); - const _frequency = Math.trunc(toNumber(frequency, this.locale)); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertMaturityAndSettlementDatesAreValid(start, end); - assertCouponFrequencyIsValid(_frequency); - assertDayCountConventionIsValid(_dayCountConvention); - // https://wiki.documentfoundation.org/Documentation/Calc_Functions/COUPDAYS - if (_dayCountConvention === 1) { - const before = COUPPCD.compute.bind(this)(settlement, maturity, frequency, dayCountConvention); - const after = COUPNCD.compute.bind(this)(settlement, maturity, frequency, dayCountConvention); - return after - before; - } - const daysInYear = _dayCountConvention === 3 ? 365 : 360; - return daysInYear / _frequency; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COUPDAYBS - // ----------------------------------------------------------------------------- - const COUPDAYBS = { - description: _t("Days from settlement until next coupon."), - args: COUPON_FUNCTION_ARGS, - returns: ["NUMBER"], - compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const start = Math.trunc(toNumber(settlement, this.locale)); - const end = Math.trunc(toNumber(maturity, this.locale)); - const _frequency = Math.trunc(toNumber(frequency, this.locale)); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertMaturityAndSettlementDatesAreValid(start, end); - assertCouponFrequencyIsValid(_frequency); - assertDayCountConventionIsValid(_dayCountConvention); - const couponBeforeStart = COUPPCD.compute.bind(this)(start, end, frequency, dayCountConvention); - if ([1, 2, 3].includes(_dayCountConvention)) { - return start - couponBeforeStart; - } - if (_dayCountConvention === 4) { - const yearFrac = getYearFrac(couponBeforeStart, start, _dayCountConvention); - return Math.round(yearFrac * 360); - } - const startDate = toJsDate(start, this.locale); - const dateCouponBeforeStart = toJsDate(couponBeforeStart, this.locale); - const y1 = dateCouponBeforeStart.getFullYear(); - const y2 = startDate.getFullYear(); - const m1 = dateCouponBeforeStart.getMonth() + 1; // +1 because months in js start at 0 and it's confusing - const m2 = startDate.getMonth() + 1; - let d1 = dateCouponBeforeStart.getDate(); - let d2 = startDate.getDate(); - /** - * Rules based on https://en.wikipedia.org/wiki/Day_count_convention#30/360_US - * - * These are slightly modified (no mention of if investment is EOM and rules order is modified), - * but from my testing this seems the rules used by Excel/GSheet. - */ - if (m1 === 2 && - m2 === 2 && - isLastDayOfMonth(dateCouponBeforeStart) && - isLastDayOfMonth(startDate)) { - d2 = 30; - } - if (d2 === 31 && (d1 === 30 || d1 === 31)) { - d2 = 30; - } - if (m1 === 2 && isLastDayOfMonth(dateCouponBeforeStart)) { - d1 = 30; - } - if (d1 === 31) { - d1 = 30; - } - return (y2 - y1) * 360 + (m2 - m1) * 30 + (d2 - d1); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COUPDAYSNC - // ----------------------------------------------------------------------------- - const COUPDAYSNC = { - description: _t("Days from settlement until next coupon."), - args: COUPON_FUNCTION_ARGS, - returns: ["NUMBER"], - compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const start = Math.trunc(toNumber(settlement, this.locale)); - const end = Math.trunc(toNumber(maturity, this.locale)); - const _frequency = Math.trunc(toNumber(frequency, this.locale)); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertMaturityAndSettlementDatesAreValid(start, end); - assertCouponFrequencyIsValid(_frequency); - assertDayCountConventionIsValid(_dayCountConvention); - const couponAfterStart = COUPNCD.compute.bind(this)(start, end, frequency, dayCountConvention); - if ([1, 2, 3].includes(_dayCountConvention)) { - return couponAfterStart - start; - } - if (_dayCountConvention === 4) { - const yearFrac = getYearFrac(start, couponAfterStart, _dayCountConvention); - return Math.round(yearFrac * 360); - } - const coupDayBs = COUPDAYBS.compute.bind(this)(settlement, maturity, frequency, _dayCountConvention); - const coupDays = COUPDAYS.compute.bind(this)(settlement, maturity, frequency, _dayCountConvention); - return coupDays - coupDayBs; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COUPNCD - // ----------------------------------------------------------------------------- - const COUPNCD = { - description: _t("Next coupon date after the settlement date."), - args: COUPON_FUNCTION_ARGS, - returns: ["NUMBER"], - computeFormat: function () { - return this.locale.dateFormat; - }, - compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const start = Math.trunc(toNumber(settlement, this.locale)); - const end = Math.trunc(toNumber(maturity, this.locale)); - const _frequency = Math.trunc(toNumber(frequency, this.locale)); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertMaturityAndSettlementDatesAreValid(start, end); - assertCouponFrequencyIsValid(_frequency); - assertDayCountConventionIsValid(_dayCountConvention); - const monthsPerPeriod = 12 / _frequency; - const coupNum = COUPNUM.compute.bind(this)(settlement, maturity, frequency, dayCountConvention); - const date = addMonthsToDate(toJsDate(end, this.locale), -(coupNum - 1) * monthsPerPeriod, true); - return jsDateToRoundNumber(date); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COUPNUM - // ----------------------------------------------------------------------------- - const COUPNUM = { - description: _t("Number of coupons between settlement and maturity."), - args: COUPON_FUNCTION_ARGS, - returns: ["NUMBER"], - compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const start = Math.trunc(toNumber(settlement, this.locale)); - const end = Math.trunc(toNumber(maturity, this.locale)); - const _frequency = Math.trunc(toNumber(frequency, this.locale)); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertMaturityAndSettlementDatesAreValid(start, end); - assertCouponFrequencyIsValid(_frequency); - assertDayCountConventionIsValid(_dayCountConvention); - let num = 1; - let currentDate = end; - const monthsPerPeriod = 12 / _frequency; - while (currentDate > start) { - currentDate = jsDateToRoundNumber(addMonthsToDate(toJsDate(currentDate, this.locale), -monthsPerPeriod, false)); - num++; - } - return num - 1; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COUPPCD - // ----------------------------------------------------------------------------- - const COUPPCD = { - description: _t("Last coupon date prior to or on the settlement date."), - args: COUPON_FUNCTION_ARGS, - returns: ["NUMBER"], - computeFormat: function () { - return this.locale.dateFormat; - }, - compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const start = Math.trunc(toNumber(settlement, this.locale)); - const end = Math.trunc(toNumber(maturity, this.locale)); - const _frequency = Math.trunc(toNumber(frequency, this.locale)); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertMaturityAndSettlementDatesAreValid(start, end); - assertCouponFrequencyIsValid(_frequency); - assertDayCountConventionIsValid(_dayCountConvention); - const monthsPerPeriod = 12 / _frequency; - const coupNum = COUPNUM.compute.bind(this)(settlement, maturity, frequency, dayCountConvention); - const date = addMonthsToDate(toJsDate(end, this.locale), -coupNum * monthsPerPeriod, true); - return jsDateToRoundNumber(date); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // CUMIPMT - // ----------------------------------------------------------------------------- - const CUMIPMT = { - description: _t("Cumulative interest paid over a set of periods."), - args: [ - arg("rate (number)", _t("The interest rate.")), - arg("number_of_periods (number)", _t("The number of payments to be made.")), - arg("present_value (number)", _t("The current value of the annuity.")), - arg("first_period (number)", _t("The number of the payment period to begin the cumulative calculation.")), - arg("last_period (number)", _t("The number of the payment period to end the cumulative calculation.")), - arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), - ], - returns: ["NUMBER"], - compute: function (rate, numberOfPeriods, presentValue, firstPeriod, lastPeriod, endOrBeginning = DEFAULT_END_OR_BEGINNING) { - const first = toNumber(firstPeriod, this.locale); - const last = toNumber(lastPeriod, this.locale); - const _rate = toNumber(rate, this.locale); - const pv = toNumber(presentValue, this.locale); - const nOfPeriods = toNumber(numberOfPeriods, this.locale); - assertFirstAndLastPeriodsAreValid(first, last, nOfPeriods); - assertRateStrictlyPositive(_rate); - assertPresentValueStrictlyPositive(pv); - let cumSum = 0; - for (let i = first; i <= last; i++) { - const impt = IPMT.compute.bind(this)(rate, i, nOfPeriods, presentValue, 0, endOrBeginning); - cumSum += impt; - } - return cumSum; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // CUMPRINC - // ----------------------------------------------------------------------------- - const CUMPRINC = { - description: _t("Cumulative principal paid over a set of periods."), - args: [ - arg("rate (number)", _t("The interest rate.")), - arg("number_of_periods (number)", _t("The number of payments to be made.")), - arg("present_value (number)", _t("The current value of the annuity.")), - arg("first_period (number)", _t("The number of the payment period to begin the cumulative calculation.")), - arg("last_period (number)", _t("The number of the payment period to end the cumulative calculation.")), - arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), - ], - returns: ["NUMBER"], - compute: function (rate, numberOfPeriods, presentValue, firstPeriod, lastPeriod, endOrBeginning = DEFAULT_END_OR_BEGINNING) { - const first = toNumber(firstPeriod, this.locale); - const last = toNumber(lastPeriod, this.locale); - const _rate = toNumber(rate, this.locale); - const pv = toNumber(presentValue, this.locale); - const nOfPeriods = toNumber(numberOfPeriods, this.locale); - assertFirstAndLastPeriodsAreValid(first, last, nOfPeriods); - assertRateStrictlyPositive(_rate); - assertPresentValueStrictlyPositive(pv); - let cumSum = 0; - for (let i = first; i <= last; i++) { - const ppmt = PPMT.compute.bind(this)(rate, i, nOfPeriods, presentValue, 0, endOrBeginning); - cumSum += ppmt; - } - return cumSum; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DB - // ----------------------------------------------------------------------------- - const DB = { - description: _t("Depreciation via declining balance method."), - args: [ - arg("cost (number)", _t("The initial cost of the asset.")), - arg("salvage (number)", _t("The value of the asset at the end of depreciation.")), - arg("life (number)", _t("The number of periods over which the asset is depreciated.")), - arg("period (number)", _t("The single period within life for which to calculate depreciation.")), - arg("month (number, optional)", _t("The number of months in the first year of depreciation.")), - ], - returns: ["NUMBER"], - // to do: replace by dollar format - computeFormat: () => "#,##0.00", - compute: function (cost, salvage, life, period, ...args) { - const _cost = toNumber(cost, this.locale); - const _salvage = toNumber(salvage, this.locale); - const _life = toNumber(life, this.locale); - const _period = Math.trunc(toNumber(period, this.locale)); - const _month = args.length ? Math.trunc(toNumber(args[0], this.locale)) : 12; - const lifeLimit = _life + (_month === 12 ? 0 : 1); - assertCostPositiveOrZero(_cost); - assertSalvagePositiveOrZero(_salvage); - assertPeriodStrictlyPositive(_period); - assertLifeStrictlyPositive(_life); - assert(() => 1 <= _month && _month <= 12, _t("The month (%s) must be between 1 and 12 inclusive.", _month.toString())); - assert(() => _period <= lifeLimit, _t("The period (%s) must be less than or equal to %s.", _period.toString(), lifeLimit.toString())); - const monthPart = _month / 12; - let rate = 1 - Math.pow(_salvage / _cost, 1 / _life); - // round to 3 decimal places - rate = Math.round(rate * 1000) / 1000; - let before = _cost; - let after = _cost * (1 - rate * monthPart); - for (let i = 1; i < _period; i++) { - before = after; - after = before * (1 - rate); - if (i === _life) { - after = before * (1 - rate * (1 - monthPart)); - } - } - return before - after; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DDB - // ----------------------------------------------------------------------------- - const DEFAULT_DDB_DEPRECIATION_FACTOR = 2; - const DDB = { - description: _t("Depreciation via double-declining balance method."), - args: [ - arg("cost (number)", _t("The initial cost of the asset.")), - arg("salvage (number)", _t("The value of the asset at the end of depreciation.")), - arg("life (number)", _t("The number of periods over which the asset is depreciated.")), - arg("period (number)", _t("The single period within life for which to calculate depreciation.")), - arg(`factor (number, default=${DEFAULT_DDB_DEPRECIATION_FACTOR})`, _t("The factor by which depreciation decreases.")), - ], - returns: ["NUMBER"], - computeFormat: () => "#,##0.00", - compute: function (cost, salvage, life, period, factor = DEFAULT_DDB_DEPRECIATION_FACTOR) { - factor = factor || 0; - const _cost = toNumber(cost, this.locale); - const _salvage = toNumber(salvage, this.locale); - const _life = toNumber(life, this.locale); - const _period = toNumber(period, this.locale); - const _factor = toNumber(factor, this.locale); - assertCostPositiveOrZero(_cost); - assertSalvagePositiveOrZero(_salvage); - assertPeriodStrictlyPositive(_period); - assertLifeStrictlyPositive(_life); - assertPeriodSmallerOrEqualToLife(_period, _life); - assertDeprecationFactorStrictlyPositive(_factor); - if (_cost === 0 || _salvage >= _cost) - return 0; - const deprecFactor = _factor / _life; - if (deprecFactor > 1) { - return period === 1 ? _cost - _salvage : 0; - } - if (_period <= 1) { - return _cost * deprecFactor; - } - const previousCost = _cost * Math.pow(1 - deprecFactor, _period - 1); - const nextCost = _cost * Math.pow(1 - deprecFactor, _period); - const deprec = nextCost < _salvage ? previousCost - _salvage : previousCost - nextCost; - return Math.max(deprec, 0); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DISC - // ----------------------------------------------------------------------------- - const DISC = { - description: _t("Discount rate of a security based on price."), - args: [ - arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), - arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), - arg("price (number)", _t("The price at which the security is bought per 100 face value.")), - arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")), - arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), - ], - returns: ["NUMBER"], - compute: function (settlement, maturity, price, redemption, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const _settlement = Math.trunc(toNumber(settlement, this.locale)); - const _maturity = Math.trunc(toNumber(maturity, this.locale)); - const _price = toNumber(price, this.locale); - const _redemption = toNumber(redemption, this.locale); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); - assertDayCountConventionIsValid(_dayCountConvention); - assertPriceStrictlyPositive(_price); - assertRedemptionStrictlyPositive(_redemption); - /** - * https://support.microsoft.com/en-us/office/disc-function-71fce9f3-3f05-4acf-a5a3-eac6ef4daa53 - * - * B = number of days in year, depending on year basis - * DSM = number of days from settlement to maturity - * - * redemption - price B - * DISC = ____________________ * ____ - * redemption DSM - */ - const yearsFrac = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention); - return (_redemption - _price) / _redemption / yearsFrac; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DOLLARDE - // ----------------------------------------------------------------------------- - const DOLLARDE = { - description: _t("Convert a decimal fraction to decimal value."), - args: [ - arg("fractional_price (number)", _t("The price quotation given using fractional decimal conventions.")), - arg("unit (number)", _t("The units of the fraction, e.g. 8 for 1/8ths or 32 for 1/32nds.")), - ], - returns: ["NUMBER"], - compute: function (fractionalPrice, unit) { - const price = toNumber(fractionalPrice, this.locale); - const _unit = Math.trunc(toNumber(unit, this.locale)); - assert(() => _unit > 0, _t("The unit (%s) must be strictly positive.", _unit.toString())); - const truncatedPrice = Math.trunc(price); - const priceFractionalPart = price - truncatedPrice; - const frac = 10 ** Math.ceil(Math.log10(_unit)) / _unit; - return truncatedPrice + priceFractionalPart * frac; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DOLLARFR - // ----------------------------------------------------------------------------- - const DOLLARFR = { - description: _t("Convert a decimal value to decimal fraction."), - args: [ - arg("decimal_price (number)", _t("The price quotation given as a decimal value.")), - arg("unit (number)", _t("The units of the desired fraction, e.g. 8 for 1/8ths or 32 for 1/32nds.")), - ], - returns: ["NUMBER"], - compute: function (decimalPrice, unit) { - const price = toNumber(decimalPrice, this.locale); - const _unit = Math.trunc(toNumber(unit, this.locale)); - assert(() => _unit > 0, _t("The unit (%s) must be strictly positive.", _unit.toString())); - const truncatedPrice = Math.trunc(price); - const priceFractionalPart = price - truncatedPrice; - const frac = _unit / 10 ** Math.ceil(Math.log10(_unit)); - return truncatedPrice + priceFractionalPart * frac; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DURATION - // ----------------------------------------------------------------------------- - const DURATION = { - description: _t("Number of periods for an investment to reach a value."), - args: [ - arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), - arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), - arg("rate (number)", _t("The annualized rate of interest.")), - arg("yield (number)", _t("The expected annual yield of the security.")), - arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4).")), - arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), - ], - returns: ["NUMBER"], - compute: function (settlement, maturity, rate, securityYield, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const start = Math.trunc(toNumber(settlement, this.locale)); - const end = Math.trunc(toNumber(maturity, this.locale)); - const _rate = toNumber(rate, this.locale); - const _yield = toNumber(securityYield, this.locale); - const _frequency = Math.trunc(toNumber(frequency, this.locale)); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertMaturityAndSettlementDatesAreValid(start, end); - assertCouponFrequencyIsValid(_frequency); - assertDayCountConventionIsValid(_dayCountConvention); - assert(() => _rate >= 0, _t("The rate (%s) must be positive or null.", _rate.toString())); - assert(() => _yield >= 0, _t("The yield (%s) must be positive or null.", _yield.toString())); - const years = YEARFRAC.compute.bind(this)(start, end, _dayCountConvention); - const timeFirstYear = years - Math.trunc(years) || 1 / _frequency; - const nbrCoupons = Math.ceil(years * _frequency); - // The DURATION function return the Macaulay duration - // See example: https://en.wikipedia.org/wiki/Bond_duration#Formulas - const cashFlowFromCoupon = _rate / _frequency; - const yieldPerPeriod = _yield / _frequency; - let count = 0; - let sum = 0; - for (let i = 1; i <= nbrCoupons; i++) { - const cashFlowPerPeriod = cashFlowFromCoupon + (i === nbrCoupons ? 1 : 0); - const presentValuePerPeriod = cashFlowPerPeriod / (1 + yieldPerPeriod) ** i; - sum += (timeFirstYear + (i - 1) / _frequency) * presentValuePerPeriod; - count += presentValuePerPeriod; - } - return count === 0 ? 0 : sum / count; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // EFFECT - // ----------------------------------------------------------------------------- - const EFFECT = { - description: _t("Annual effective interest rate."), - args: [ - arg("nominal_rate (number)", _t("The nominal interest rate per year.")), - arg("periods_per_year (number)", _t("The number of compounding periods per year.")), - ], - returns: ["NUMBER"], - compute: function (nominal_rate, periods_per_year) { - const nominal = toNumber(nominal_rate, this.locale); - const periods = Math.trunc(toNumber(periods_per_year, this.locale)); - assert(() => nominal > 0, _t("The nominal rate (%s) must be strictly greater than 0.", nominal.toString())); - assert(() => periods > 0, _t("The number of periods by year (%s) must strictly greater than 0.", periods.toString())); - // https://en.wikipedia.org/wiki/Nominal_interest_rate#Nominal_versus_effective_interest_rate - return Math.pow(1 + nominal / periods, periods) - 1; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // FV - // ----------------------------------------------------------------------------- - const DEFAULT_PRESENT_VALUE = 0; - const FV = { - description: _t("Future value of an annuity investment."), - args: [ - arg("rate (number)", _t("The interest rate.")), - arg("number_of_periods (number)", _t("The number of payments to be made.")), - arg("payment_amount (number)", _t("The amount per period to be paid.")), - arg(`present_value (number, default=${DEFAULT_PRESENT_VALUE})`, _t("The current value of the annuity.")), - arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), - ], - returns: ["NUMBER"], - // to do: replace by dollar format - computeFormat: () => "#,##0.00", - compute: function (rate, numberOfPeriods, paymentAmount, presentValue = DEFAULT_PRESENT_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) { - presentValue = presentValue || 0; - endOrBeginning = endOrBeginning || 0; - const r = toNumber(rate, this.locale); - const n = toNumber(numberOfPeriods, this.locale); - const p = toNumber(paymentAmount, this.locale); - const pv = toNumber(presentValue, this.locale); - const type = toBoolean(endOrBeginning) ? 1 : 0; - return r ? -pv * (1 + r) ** n - (p * (1 + r * type) * ((1 + r) ** n - 1)) / r : -(pv + p * n); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // FVSCHEDULE - // ----------------------------------------------------------------------------- - const FVSCHEDULE = { - description: _t("Future value of principal from series of rates."), - args: [ - arg("principal (number)", _t("The amount of initial capital or value to compound against.")), - arg("rate_schedule (number, range)", _t("A series of interest rates to compound against the principal.")), - ], - returns: ["NUMBER"], - compute: function (principalAmount, rateSchedule) { - const principal = toNumber(principalAmount, this.locale); - return reduceAny([rateSchedule], (acc, rate) => acc * (1 + toNumber(rate, this.locale)), principal); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // INTRATE - // ----------------------------------------------------------------------------- - const INTRATE = { - description: _t("Calculates effective interest rate."), - args: [ - arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), - arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), - arg("investment (number)", _t("The amount invested in the security.")), - arg("redemption (number)", _t("The amount to be received at maturity.")), - arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), - ], - returns: ["NUMBER"], - compute: function (settlement, maturity, investment, redemption, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - const _settlement = Math.trunc(toNumber(settlement, this.locale)); - const _maturity = Math.trunc(toNumber(maturity, this.locale)); - const _redemption = toNumber(redemption, this.locale); - const _investment = toNumber(investment, this.locale); - assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); - assertInvestmentStrictlyPositive(_investment); - assertRedemptionStrictlyPositive(_redemption); - /** - * https://wiki.documentfoundation.org/Documentation/Calc_Functions/INTRATE - * - * (Redemption - Investment) / Investment - * INTRATE = _________________________________________ - * YEARFRAC(settlement, maturity, basis) - */ - const yearFrac = YEARFRAC.compute.bind(this)(_settlement, _maturity, dayCountConvention); - return (_redemption - _investment) / _investment / yearFrac; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // IPMT - // ----------------------------------------------------------------------------- - const IPMT = { - description: _t("Payment on the principal of an investment."), - args: [ - arg("rate (number)", _t("The annualized rate of interest.")), - arg("period (number)", _t("The amortization period, in terms of number of periods.")), - arg("number_of_periods (number)", _t("The number of payments to be made.")), - arg("present_value (number)", _t("The current value of the annuity.")), - arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")), - arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), - ], - returns: ["NUMBER"], - computeFormat: () => "#,##0.00", - compute: function (rate, currentPeriod, numberOfPeriods, presentValue, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) { - const payment = PMT.compute.bind(this)(rate, numberOfPeriods, presentValue, futureValue, endOrBeginning); - const ppmt = PPMT.compute.bind(this)(rate, currentPeriod, numberOfPeriods, presentValue, futureValue, endOrBeginning); - return payment - ppmt; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // IRR - // ----------------------------------------------------------------------------- - const DEFAULT_RATE_GUESS = 0.1; - const IRR = { - description: _t("Internal rate of return given periodic cashflows."), - args: [ - arg("cashflow_amounts (number, range)", _t("An array or range containing the income or payments associated with the investment.")), - arg(`rate_guess (number, default=${DEFAULT_RATE_GUESS})`, _t("An estimate for what the internal rate of return will be.")), - ], - returns: ["NUMBER"], - computeFormat: () => "0%", - compute: function (cashFlowAmounts, rateGuess = DEFAULT_RATE_GUESS) { - const _rateGuess = toNumber(rateGuess, this.locale); - assertRateGuessStrictlyGreaterThanMinusOne(_rateGuess); - // check that values contains at least one positive value and one negative value - // and extract number present in the cashFlowAmount argument - let positive = false; - let negative = false; - let amounts = []; - visitNumbers([cashFlowAmounts], (amount) => { - if (amount > 0) - positive = true; - if (amount < 0) - negative = true; - amounts.push(amount); - }, this.locale); - assert(() => positive && negative, _t("The cashflow_amounts must include negative and positive values.")); - const firstAmount = amounts.shift(); - // The result of IRR is the rate at which the NPV() function will return zero with the given values. - // This algorithm uses the Newton's method on the NPV function to determine the result - // Newton's method: https://en.wikipedia.org/wiki/Newton%27s_method - // As the NPV function isn't continuous, we apply the Newton's method on the numerator of the NPV formula. - function npvNumerator(rate, startValue, values) { - const nbrValue = values.length; - let i = 0; - return values.reduce((acc, v) => { - i++; - return acc + v * rate ** (nbrValue - i); - }, startValue * rate ** nbrValue); - } - function npvNumeratorDeriv(rate, startValue, values) { - const nbrValue = values.length; - let i = 0; - return values.reduce((acc, v) => { - i++; - return acc + v * (nbrValue - i) * rate ** (nbrValue - i - 1); - }, startValue * nbrValue * rate ** (nbrValue - 1)); - } - function func(x) { - return npvNumerator(x, firstAmount, amounts); - } - function derivFunc(x) { - return npvNumeratorDeriv(x, firstAmount, amounts); - } - return newtonMethod(func, derivFunc, _rateGuess + 1, 20, 1e-5) - 1; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ISPMT - // ----------------------------------------------------------------------------- - const ISPMT = { - description: _t("Returns the interest paid at a particular period of an investment."), - args: [ - arg("rate (number)", _t("The interest rate.")), - arg("period (number)", _t("The period for which you want to view the interest payment.")), - arg("number_of_periods (number)", _t("The number of payments to be made.")), - arg("present_value (number)", _t("The current value of the annuity.")), - ], - returns: ["NUMBER"], - compute: function (rate, currentPeriod, numberOfPeriods, presentValue) { - const interestRate = toNumber(rate, this.locale); - const period = toNumber(currentPeriod, this.locale); - const nOfPeriods = toNumber(numberOfPeriods, this.locale); - const investment = toNumber(presentValue, this.locale); - assert(() => nOfPeriods !== 0, _t("The number of periods must be different than 0.", nOfPeriods.toString())); - const currentInvestment = investment - investment * (period / nOfPeriods); - return -1 * currentInvestment * interestRate; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MDURATION - // ----------------------------------------------------------------------------- - const MDURATION = { - description: _t("Modified Macaulay duration."), - args: [ - arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), - arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), - arg("rate (number)", _t("The annualized rate of interest.")), - arg("yield (number)", _t("The expected annual yield of the security.")), - arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4).")), - arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), - ], - returns: ["NUMBER"], - compute: function (settlement, maturity, rate, securityYield, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - const duration = DURATION.compute.bind(this)(settlement, maturity, rate, securityYield, frequency, dayCountConvention); - const y = toNumber(securityYield, this.locale); - const k = Math.trunc(toNumber(frequency, this.locale)); - return duration / (1 + y / k); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MIRR - // ----------------------------------------------------------------------------- - const MIRR = { - description: _t("Modified internal rate of return."), - args: [ - arg("cashflow_amounts (range)", _t("A range containing the income or payments associated with the investment. The array should contain bot payments and incomes.")), - arg("financing_rate (number)", _t("The interest rate paid on funds invested.")), - arg("reinvestment_return_rate (number)", _t("The return (as a percentage) earned on reinvestment of income received from the investment.")), - ], - returns: ["NUMBER"], - compute: function (cashflowAmount, financingRate, reinvestmentRate) { - const fRate = toNumber(financingRate, this.locale); - const rRate = toNumber(reinvestmentRate, this.locale); - const cashFlow = transposeMatrix(cashflowAmount) - .flat() - .filter((t) => t !== null) - .map((val) => toNumber(val, this.locale)); - const n = cashFlow.length; - /** - * https://en.wikipedia.org/wiki/Modified_internal_rate_of_return - * - * / FV(positive cash flows, reinvestment rate) \ ^ (1 / (n - 1)) - * MIRR = | ___________________________________________ | - 1 - * \ - PV(negative cash flows, finance rate) / - * - * with n the number of cash flows. - * - * You can compute FV and PV as : - * - * FV = SUM [ (cashFlow[i]>0 ? cashFlow[i] : 0) * (1 + rRate)**(n - i-1) ] - * i= 0 => n - * - * PV = SUM [ (cashFlow[i]<0 ? cashFlow[i] : 0) / (1 + fRate)**i ] - * i= 0 => n - */ - let fv = 0; - let pv = 0; - for (const i of range(0, n)) { - const amount = cashFlow[i]; - if (amount >= 0) { - fv += amount * (rRate + 1) ** (n - i - 1); - } - else { - pv += amount / (fRate + 1) ** i; - } - } - assert(() => pv !== 0 && fv !== 0, _t("There must be both positive and negative values in cashflow_amounts.")); - const exponent = 1 / (n - 1); - return (-fv / pv) ** exponent - 1; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // NOMINAL - // ----------------------------------------------------------------------------- - const NOMINAL = { - description: _t("Annual nominal interest rate."), - args: [ - arg("effective_rate (number)", _t("The effective interest rate per year.")), - arg("periods_per_year (number)", _t("The number of compounding periods per year.")), - ], - returns: ["NUMBER"], - compute: function (effective_rate, periods_per_year) { - const effective = toNumber(effective_rate, this.locale); - const periods = Math.trunc(toNumber(periods_per_year, this.locale)); - assert(() => effective > 0, _t("The effective rate (%s) must must strictly greater than 0.", effective.toString())); - assert(() => periods > 0, _t("The number of periods by year (%s) must strictly greater than 0.", periods.toString())); - // https://en.wikipedia.org/wiki/Nominal_interest_rate#Nominal_versus_effective_interest_rate - return (Math.pow(effective + 1, 1 / periods) - 1) * periods; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // NPER - // ----------------------------------------------------------------------------- - const NPER = { - description: _t("Number of payment periods for an investment."), - args: [ - arg("rate (number)", _t("The interest rate.")), - arg("payment_amount (number)", _t("The amount of each payment made.")), - arg("present_value (number)", _t("The current value of the annuity.")), - arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")), - arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), - ], - returns: ["NUMBER"], - compute: function (rate, paymentAmount, presentValue, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) { - futureValue = futureValue || 0; - endOrBeginning = endOrBeginning || 0; - const r = toNumber(rate, this.locale); - const p = toNumber(paymentAmount, this.locale); - const pv = toNumber(presentValue, this.locale); - const fv = toNumber(futureValue, this.locale); - const t = toBoolean(endOrBeginning) ? 1 : 0; - /** - * https://wiki.documentfoundation.org/Documentation/Calc_Functions/NPER - * - * 0 = pv * (1 + r)^N + fv + [ p * (1 + r * t) * ((1 + r)^N - 1) ] / r - * - * We solve the equation for N: - * - * with C = [ p * (1 + r * t)] / r and - * R = 1 + r - * - * => 0 = pv * R^N + C * R^N - C + fv - * <=> (C - fv) = R^N * (pv + C) - * <=> log[(C - fv) / (pv + C)] = N * log(R) - */ - if (r === 0) { - return -(fv + pv) / p; - } - const c = (p * (1 + r * t)) / r; - return Math.log((c - fv) / (pv + c)) / Math.log(1 + r); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // NPV - // ----------------------------------------------------------------------------- - function npvResult(r, startValue, values, locale) { - let i = 0; - return reduceNumbers(values, (acc, v) => { - i++; - return acc + v / (1 + r) ** i; - }, startValue, locale); - } - const NPV = { - description: _t("The net present value of an investment based on a series of periodic cash flows and a discount rate."), - args: [ - arg("discount (number)", _t("The discount rate of the investment over one period.")), - arg("cashflow1 (number, range)", _t("The first future cash flow.")), - arg("cashflow2 (number, range, repeating)", _t("Additional future cash flows.")), - ], - returns: ["NUMBER"], - // to do: replace by dollar format - computeFormat: () => "#,##0.00", - compute: function (discount, ...values) { - const _discount = toNumber(discount, this.locale); - assert(() => _discount !== -1, _t("The discount (%s) must be different from -1.", _discount.toString())); - return npvResult(_discount, 0, values, this.locale); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // PDURATION - // ----------------------------------------------------------------------------- - const PDURATION = { - description: _t("Computes the number of periods needed for an investment to reach a value."), - args: [ - arg("rate (number)", _t("The rate at which the investment grows each period.")), - arg("present_value (number)", _t("The investment's current value.")), - arg("future_value (number)", _t("The investment's desired future value.")), - ], - returns: ["NUMBER"], - compute: function (rate, presentValue, futureValue) { - const _rate = toNumber(rate, this.locale); - const _presentValue = toNumber(presentValue, this.locale); - const _futureValue = toNumber(futureValue, this.locale); - assertRateStrictlyPositive(_rate); - assert(() => _presentValue > 0, _t("The present_value (%s) must be strictly positive.", _presentValue.toString())); - assert(() => _futureValue > 0, _t("The future_value (%s) must be strictly positive.", _futureValue.toString())); - return (Math.log(_futureValue) - Math.log(_presentValue)) / Math.log(1 + _rate); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // PMT - // ----------------------------------------------------------------------------- - const PMT = { - description: _t("Periodic payment for an annuity investment."), - args: [ - arg("rate (number)", _t("The annualized rate of interest.")), - arg("number_of_periods (number)", _t("The number of payments to be made.")), - arg("present_value (number)", _t("The current value of the annuity.")), - arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")), - arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), - ], - returns: ["NUMBER"], - computeFormat: () => "#,##0.00", - compute: function (rate, numberOfPeriods, presentValue, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) { - futureValue = futureValue || 0; - endOrBeginning = endOrBeginning || 0; - const n = toNumber(numberOfPeriods, this.locale); - const r = toNumber(rate, this.locale); - const t = toBoolean(endOrBeginning) ? 1 : 0; - let fv = toNumber(futureValue, this.locale); - let pv = toNumber(presentValue, this.locale); - assertNumberOfPeriodsStrictlyPositive(n); - /** - * https://wiki.documentfoundation.org/Documentation/Calc_Functions/PMT - * - * 0 = pv * (1 + r)^N + fv + [ p * (1 + r * t) * ((1 + r)^N - 1) ] / r - * - * We simply the equation for p - */ - if (r === 0) { - return -(fv + pv) / n; - } - let payment = -(pv * (1 + r) ** n + fv); - payment = (payment * r) / ((1 + r * t) * ((1 + r) ** n - 1)); - return payment; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // PPMT - // ----------------------------------------------------------------------------- - const PPMT = { - description: _t("Payment on the principal of an investment."), - args: [ - arg("rate (number)", _t("The annualized rate of interest.")), - arg("period (number)", _t("The amortization period, in terms of number of periods.")), - arg("number_of_periods (number)", _t("The number of payments to be made.")), - arg("present_value (number)", _t("The current value of the annuity.")), - arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")), - arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), - ], - returns: ["NUMBER"], - computeFormat: () => "#,##0.00", - compute: function (rate, currentPeriod, numberOfPeriods, presentValue, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) { - futureValue = futureValue || 0; - endOrBeginning = endOrBeginning || 0; - const n = toNumber(numberOfPeriods, this.locale); - const r = toNumber(rate, this.locale); - const period = toNumber(currentPeriod, this.locale); - const type = toBoolean(endOrBeginning) ? 1 : 0; - const fv = toNumber(futureValue, this.locale); - const pv = toNumber(presentValue, this.locale); - assertNumberOfPeriodsStrictlyPositive(n); - assert(() => period > 0 && period <= n, _t("The period must be between 1 and number_of_periods", n.toString())); - const payment = PMT.compute.bind(this)(r, n, pv, fv, endOrBeginning); - if (type === 1 && period === 1) - return payment; - const eqPeriod = type === 0 ? period - 1 : period - 2; - const eqPv = pv + payment * type; - const capitalAtPeriod = -FV.compute.bind(this)(r, eqPeriod, payment, eqPv, 0); - const currentInterest = capitalAtPeriod * r; - return payment + currentInterest; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // PV - // ----------------------------------------------------------------------------- - const PV = { - description: _t("Present value of an annuity investment."), - args: [ - arg("rate (number)", _t("The interest rate.")), - arg("number_of_periods (number)", _t("The number of payments to be made.")), - arg("payment_amount (number)", _t("The amount per period to be paid.")), - arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")), - arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), - ], - returns: ["NUMBER"], - // to do: replace by dollar format - computeFormat: () => "#,##0.00", - compute: function (rate, numberOfPeriods, paymentAmount, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) { - futureValue = futureValue || 0; - endOrBeginning = endOrBeginning || 0; - const r = toNumber(rate, this.locale); - const n = toNumber(numberOfPeriods, this.locale); - const p = toNumber(paymentAmount, this.locale); - const fv = toNumber(futureValue, this.locale); - const type = toBoolean(endOrBeginning) ? 1 : 0; - // https://wiki.documentfoundation.org/Documentation/Calc_Functions/PV - return r ? -((p * (1 + r * type) * ((1 + r) ** n - 1)) / r + fv) / (1 + r) ** n : -(fv + p * n); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // PRICE - // ----------------------------------------------------------------------------- - const PRICE = { - description: _t("Price of a security paying periodic interest."), - args: [ - arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), - arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), - arg("rate (number)", _t("The annualized rate of interest.")), - arg("yield (number)", _t("The expected annual yield of the security.")), - arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")), - arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4).")), - arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), - ], - returns: ["NUMBER"], - compute: function (settlement, maturity, rate, securityYield, redemption, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const _settlement = Math.trunc(toNumber(settlement, this.locale)); - const _maturity = Math.trunc(toNumber(maturity, this.locale)); - const _rate = toNumber(rate, this.locale); - const _yield = toNumber(securityYield, this.locale); - const _redemption = toNumber(redemption, this.locale); - const _frequency = Math.trunc(toNumber(frequency, this.locale)); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); - assertCouponFrequencyIsValid(_frequency); - assertDayCountConventionIsValid(_dayCountConvention); - assert(() => _rate >= 0, _t("The rate (%s) must be positive or null.", _rate.toString())); - assert(() => _yield >= 0, _t("The yield (%s) must be positive or null.", _yield.toString())); - assertRedemptionStrictlyPositive(_redemption); - const years = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention); - const nbrRealCoupons = years * _frequency; - const nbrFullCoupons = Math.ceil(nbrRealCoupons); - const timeFirstCoupon = nbrRealCoupons - Math.floor(nbrRealCoupons) || 1; - const yieldFactorPerPeriod = 1 + _yield / _frequency; - const cashFlowFromCoupon = (100 * _rate) / _frequency; - if (nbrFullCoupons === 1) { - return ((cashFlowFromCoupon + _redemption) / ((timeFirstCoupon * _yield) / _frequency + 1) - - cashFlowFromCoupon * (1 - timeFirstCoupon)); - } - let cashFlowsPresentValue = 0; - for (let i = 1; i <= nbrFullCoupons; i++) { - cashFlowsPresentValue += - cashFlowFromCoupon / yieldFactorPerPeriod ** (i - 1 + timeFirstCoupon); - } - const redemptionPresentValue = _redemption / yieldFactorPerPeriod ** (nbrFullCoupons - 1 + timeFirstCoupon); - return (redemptionPresentValue + cashFlowsPresentValue - cashFlowFromCoupon * (1 - timeFirstCoupon)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // PRICEDISC - // ----------------------------------------------------------------------------- - const PRICEDISC = { - description: _t("Price of a discount security."), - args: [ - arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), - arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), - arg("discount (number)", _t("The discount rate of the security at time of purchase.")), - arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")), - arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), - ], - returns: ["NUMBER"], - compute: function (settlement, maturity, discount, redemption, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const _settlement = Math.trunc(toNumber(settlement, this.locale)); - const _maturity = Math.trunc(toNumber(maturity, this.locale)); - const _discount = toNumber(discount, this.locale); - const _redemption = toNumber(redemption, this.locale); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); - assertDayCountConventionIsValid(_dayCountConvention); - assertDiscountStrictlyPositive(_discount); - assertRedemptionStrictlyPositive(_redemption); - /** - * https://support.microsoft.com/en-us/office/pricedisc-function-d06ad7c1-380e-4be7-9fd9-75e3079acfd3 - * - * B = number of days in year, depending on year basis - * DSM = number of days from settlement to maturity - * - * PRICEDISC = redemption - discount * redemption * (DSM/B) - */ - const yearsFrac = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention); - return _redemption - _discount * _redemption * yearsFrac; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // PRICEMAT - // ----------------------------------------------------------------------------- - const PRICEMAT = { - description: _t("Calculates the price of a security paying interest at maturity, based on expected yield."), - args: [ - arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), - arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), - arg("issue (date)", _t("The date the security was initially issued.")), - arg("rate (number)", _t("The annualized rate of interest.")), - arg("yield (number)", _t("The expected annual yield of the security.")), - arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), - ], - returns: ["NUMBER"], - compute: function (settlement, maturity, issue, rate, securityYield, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const _settlement = Math.trunc(toNumber(settlement, this.locale)); - const _maturity = Math.trunc(toNumber(maturity, this.locale)); - const _issue = Math.trunc(toNumber(issue, this.locale)); - const _rate = toNumber(rate, this.locale); - const _yield = toNumber(securityYield, this.locale); - const _dayCount = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertSettlementAndIssueDatesAreValid(_settlement, _issue); - assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); - assertDayCountConventionIsValid(_dayCount); - assert(() => _rate >= 0, _t("The rate (%s) must be positive or null.", _rate.toString())); - assert(() => _yield >= 0, _t("The yield (%s) must be positive or null.", _yield.toString())); - /** - * https://support.microsoft.com/en-us/office/pricemat-function-52c3b4da-bc7e-476a-989f-a95f675cae77 - * - * B = number of days in year, depending on year basis - * DSM = number of days from settlement to maturity - * DIM = number of days from issue to maturity - * DIS = number of days from issue to settlement - * - * 100 + (DIM/B * rate * 100) - * PRICEMAT = __________________________ - (DIS/B * rate * 100) - * 1 + (DSM/B * yield) - * - * The ratios number_of_days / days_in_year are computed using the YEARFRAC function, that handle - * differences due to day count conventions. - * - * Compatibility note : - * - * Contrary to GSheet and OpenOffice, Excel doesn't seems to always use its own YEARFRAC function - * to compute PRICEMAT, and give different values for some combinations of dates and day count - * conventions ( notably for leap years and dayCountConvention = 1 (Actual/Actual)). - * - * Our function PRICEMAT give us the same results as LibreOffice Calc. - * Google Sheet use the formula with YEARFRAC, but its YEARFRAC function results are different - * from the results of Excel/LibreOffice, thus we get different values with PRICEMAT. - * - */ - const settlementToMaturity = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCount); - const issueToSettlement = YEARFRAC.compute.bind(this)(_settlement, _issue, _dayCount); - const issueToMaturity = YEARFRAC.compute.bind(this)(_issue, _maturity, _dayCount); - const numerator = 100 + issueToMaturity * _rate * 100; - const denominator = 1 + settlementToMaturity * _yield; - const term2 = issueToSettlement * _rate * 100; - return numerator / denominator - term2; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // RATE - // ----------------------------------------------------------------------------- - const RATE_GUESS_DEFAULT = 0.1; - const RATE = { - description: _t("Interest rate of an annuity investment."), - args: [ - arg("number_of_periods (number)", _t("The number of payments to be made.")), - arg("payment_per_period (number)", _t("The amount per period to be paid.")), - arg("present_value (number)", _t("The current value of the annuity.")), - arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")), - arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), - arg(`rate_guess (number, default=${RATE_GUESS_DEFAULT})`, _t("An estimate for what the interest rate will be.")), - ], - returns: ["NUMBER"], - computeFormat: () => "0%", - compute: function (numberOfPeriods, paymentPerPeriod, presentValue, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING, rateGuess = RATE_GUESS_DEFAULT) { - futureValue = futureValue || 0; - endOrBeginning = endOrBeginning || 0; - rateGuess = rateGuess || RATE_GUESS_DEFAULT; - const n = toNumber(numberOfPeriods, this.locale); - const payment = toNumber(paymentPerPeriod, this.locale); - const type = toBoolean(endOrBeginning) ? 1 : 0; - const guess = toNumber(rateGuess, this.locale); - let fv = toNumber(futureValue, this.locale); - let pv = toNumber(presentValue, this.locale); - assertNumberOfPeriodsStrictlyPositive(n); - assert(() => [payment, pv, fv].some((val) => val > 0) && [payment, pv, fv].some((val) => val < 0), _t("There must be both positive and negative values in [payment_amount, present_value, future_value].", n.toString())); - assertRateGuessStrictlyGreaterThanMinusOne(guess); - fv -= payment * type; - pv += payment * type; - // https://github.com/apache/openoffice/blob/trunk/main/sc/source/core/tool/interpr2.cxx - const func = (rate) => { - const powN = Math.pow(1 + rate, n); - const intResult = (powN - 1) / rate; - return fv + pv * powN + payment * intResult; - }; - const derivFunc = (rate) => { - const powNMinus1 = Math.pow(1 + rate, n - 1); - const powN = Math.pow(1 + rate, n); - const intResult = (powN - 1) / rate; - const intResultDeriv = (n * powNMinus1) / rate - intResult / rate; - const fTermDerivation = pv * n * powNMinus1 + payment * intResultDeriv; - return fTermDerivation; - }; - return newtonMethod(func, derivFunc, guess, 40, 1e-5); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // RECEIVED - // ----------------------------------------------------------------------------- - const RECEIVED = { - description: _t("Amount received at maturity for a security."), - args: [ - arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), - arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), - arg("investment (number)", _t("The amount invested (irrespective of face value of each security).")), - arg("discount (number)", _t("The discount rate of the security invested in.")), - arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), - ], - returns: ["NUMBER"], - compute: function (settlement, maturity, investment, discount, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const _settlement = Math.trunc(toNumber(settlement, this.locale)); - const _maturity = Math.trunc(toNumber(maturity, this.locale)); - const _investment = toNumber(investment, this.locale); - const _discount = toNumber(discount, this.locale); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); - assertDayCountConventionIsValid(_dayCountConvention); - assertInvestmentStrictlyPositive(_investment); - assertDiscountStrictlyPositive(_discount); - /** - * https://support.microsoft.com/en-us/office/received-function-7a3f8b93-6611-4f81-8576-828312c9b5e5 - * - * investment - * RECEIVED = _________________________ - * 1 - discount * DSM / B - * - * with DSM = number of days from settlement to maturity and B = number of days in a year - * - * The ratio DSM/B can be computed with the YEARFRAC function to take the dayCountConvention into account. - */ - const yearsFrac = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention); - return _investment / (1 - _discount * yearsFrac); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // RRI - // ----------------------------------------------------------------------------- - const RRI = { - description: _t("Computes the rate needed for an investment to reach a specific value within a specific number of periods."), - args: [ - arg("number_of_periods (number)", _t("The number of periods.")), - arg("present_value (number)", _t("The present value of the investment.")), - arg("future_value (number)", _t("The future value of the investment.")), - ], - returns: ["NUMBER"], - compute: function (numberOfPeriods, presentValue, futureValue) { - const n = toNumber(numberOfPeriods, this.locale); - const pv = toNumber(presentValue, this.locale); - const fv = toNumber(futureValue, this.locale); - assertNumberOfPeriodsStrictlyPositive(n); - /** - * https://support.microsoft.com/en-us/office/rri-function-6f5822d8-7ef1-4233-944c-79e8172930f4 - * - * RRI = (future value / present value) ^ (1 / number of periods) - 1 - */ - return (fv / pv) ** (1 / n) - 1; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SLN - // ----------------------------------------------------------------------------- - const SLN = { - description: _t("Depreciation of an asset using the straight-line method."), - args: [ - arg("cost (number)", _t("The initial cost of the asset.")), - arg("salvage (number)", _t("The value of the asset at the end of depreciation.")), - arg("life (number)", _t("The number of periods over which the asset is depreciated.")), - ], - returns: ["NUMBER"], - computeFormat: () => "#,##0.00", - compute: function (cost, salvage, life) { - const _cost = toNumber(cost, this.locale); - const _salvage = toNumber(salvage, this.locale); - const _life = toNumber(life, this.locale); - // No assertion is done on the values of the arguments to be compatible with Excel/Gsheet that don't check the values. - // It's up to the user to make sure the arguments make sense, which is good design because the user is smart. - return (_cost - _salvage) / _life; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SYD - // ----------------------------------------------------------------------------- - const SYD = { - description: _t("Depreciation via sum of years digit method."), - args: [ - arg("cost (number)", _t("The initial cost of the asset.")), - arg("salvage (number)", _t("The value of the asset at the end of depreciation.")), - arg("life (number)", _t("The number of periods over which the asset is depreciated.")), - arg("period (number)", _t("The single period within life for which to calculate depreciation.")), - ], - returns: ["NUMBER"], - computeFormat: () => "#,##0.00", - compute: function (cost, salvage, life, period) { - const _cost = toNumber(cost, this.locale); - const _salvage = toNumber(salvage, this.locale); - const _life = toNumber(life, this.locale); - const _period = toNumber(period, this.locale); - assertPeriodStrictlyPositive(_period); - assertLifeStrictlyPositive(_life); - assertPeriodSmallerOrEqualToLife(_period, _life); - /** - * This deprecation method use the sum of digits of the periods of the life as the deprecation factor. - * For example for a life = 5, we have a deprecation factor or 1 + 2 + 3 + 4 + 5 = 15 = life * (life + 1) / 2 = F. - * - * The deprecation for a period p is then computed based on F and the remaining lifetime at the period P. - * - * deprecation = (cost - salvage) * (number of remaining periods / F) - */ - const deprecFactor = (_life * (_life + 1)) / 2; - const remainingPeriods = _life - _period + 1; - return (_cost - _salvage) * (remainingPeriods / deprecFactor); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TBILLPRICE - // ----------------------------------------------------------------------------- - const TBILLPRICE = { - description: _t("Price of a US Treasury bill."), - args: [ - arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), - arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), - arg("discount (number)", _t("The discount rate of the bill at time of purchase.")), - ], - returns: ["NUMBER"], - compute: function (settlement, maturity, discount) { - const start = Math.trunc(toNumber(settlement, this.locale)); - const end = Math.trunc(toNumber(maturity, this.locale)); - const disc = toNumber(discount, this.locale); - assertMaturityAndSettlementDatesAreValid(start, end); - assertSettlementLessThanOneYearBeforeMaturity(start, end, this.locale); - assertDiscountStrictlyPositive(disc); - assertDiscountStrictlySmallerThanOne(disc); - /** - * https://support.microsoft.com/en-us/office/tbillprice-function-eacca992-c29d-425a-9eb8-0513fe6035a2 - * - * TBILLPRICE = 100 * (1 - discount * DSM / 360) - * - * with DSM = number of days from settlement to maturity - * - * The ratio DSM/360 can be computed with the YEARFRAC function with dayCountConvention = 2 (actual/360). - */ - const yearFrac = YEARFRAC.compute.bind(this)(start, end, 2); - return 100 * (1 - disc * yearFrac); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TBILLEQ - // ----------------------------------------------------------------------------- - const TBILLEQ = { - description: _t("Equivalent rate of return for a US Treasury bill."), - args: [ - arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), - arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), - arg("discount (number)", _t("The discount rate of the bill at time of purchase.")), - ], - returns: ["NUMBER"], - compute: function (settlement, maturity, discount) { - const start = Math.trunc(toNumber(settlement, this.locale)); - const end = Math.trunc(toNumber(maturity, this.locale)); - const disc = toNumber(discount, this.locale); - assertMaturityAndSettlementDatesAreValid(start, end); - assertSettlementLessThanOneYearBeforeMaturity(start, end, this.locale); - assertDiscountStrictlyPositive(disc); - assertDiscountStrictlySmallerThanOne(disc); - /** - * https://support.microsoft.com/en-us/office/tbilleq-function-2ab72d90-9b4d-4efe-9fc2-0f81f2c19c8c - * - * 365 * discount - * TBILLEQ = ________________________ - * 360 - discount * DSM - * - * with DSM = number of days from settlement to maturity - * - * What is not indicated in the Excel documentation is that this formula only works for duration between settlement - * and maturity that are less than 6 months (182 days). This is because US Treasury bills use semi-annual interest, - * and thus we have to take into account the compound interest for the calculation. - * - * For this case, the formula becomes (Treasury Securities and Derivatives, by Frank J. Fabozzi, page 49) - * - * -2X + 2* SQRT[ X² - (2X - 1) * (1 - 100/p) ] - * TBILLEQ = ________________________________________________ - * 2X - 1 - * - * with X = DSM / (number of days in a year), - * and p is the price, computed with TBILLPRICE - * - * Note that from my tests in Excel, we take (number of days in a year) = 366 ONLY if DSM is 366, not if - * the settlement year is a leap year. - * - */ - const nDays = DAYS.compute.bind(this)(end, start); - if (nDays <= 182) { - return (365 * disc) / (360 - disc * nDays); - } - const p = TBILLPRICE.compute.bind(this)(start, end, disc) / 100; - const daysInYear = nDays === 366 ? 366 : 365; - const x = nDays / daysInYear; - const num = -2 * x + 2 * Math.sqrt(x ** 2 - (2 * x - 1) * (1 - 1 / p)); - const denom = 2 * x - 1; - return num / denom; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TBILLYIELD - // ----------------------------------------------------------------------------- - const TBILLYIELD = { - description: _t("The yield of a US Treasury bill based on price."), - args: [ - arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), - arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), - arg("price (number)", _t("The price at which the security is bought per 100 face value.")), - ], - returns: ["NUMBER"], - compute: function (settlement, maturity, price) { - const start = Math.trunc(toNumber(settlement, this.locale)); - const end = Math.trunc(toNumber(maturity, this.locale)); - const p = toNumber(price, this.locale); - assertMaturityAndSettlementDatesAreValid(start, end); - assertSettlementLessThanOneYearBeforeMaturity(start, end, this.locale); - assertPriceStrictlyPositive(p); - /** - * https://support.microsoft.com/en-us/office/tbillyield-function-6d381232-f4b0-4cd5-8e97-45b9c03468ba - * - * 100 - price 360 - * TBILLYIELD = ____________ * _____ - * price DSM - * - * with DSM = number of days from settlement to maturity - * - * The ratio DSM/360 can be computed with the YEARFRAC function with dayCountConvention = 2 (actual/360). - * - */ - const yearFrac = YEARFRAC.compute.bind(this)(start, end, 2); - return ((100 - p) / p) * (1 / yearFrac); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // VDB - // ----------------------------------------------------------------------------- - const DEFAULT_VDB_NO_SWITCH = false; - const VDB = { - description: _t("Variable declining balance. WARNING : does not handle decimal periods."), - args: [ - arg("cost (number)", _t("The initial cost of the asset.")), - arg("salvage (number)", _t("The value of the asset at the end of depreciation.")), - arg("life (number)", _t("The number of periods over which the asset is depreciated.")), - arg("start (number)", _t("Starting period to calculate depreciation.")), - arg("end (number)", _t("Ending period to calculate depreciation.")), - arg(`factor (number, default=${DEFAULT_DDB_DEPRECIATION_FACTOR})`, _t("The number of months in the first year of depreciation.")), - arg(`no_switch (number, default=${DEFAULT_VDB_NO_SWITCH})`, _t("Whether to switch to straight-line depreciation when the depreciation is greater than the declining balance calculation.")), - ], - returns: ["NUMBER"], - compute: function (cost, salvage, life, startPeriod, endPeriod, factor = DEFAULT_DDB_DEPRECIATION_FACTOR, noSwitch = DEFAULT_VDB_NO_SWITCH) { - factor = factor || 0; - const _cost = toNumber(cost, this.locale); - const _salvage = toNumber(salvage, this.locale); - const _life = toNumber(life, this.locale); - /* TODO : handle decimal periods - * on end_period it looks like it is a simple linear function, but I cannot understand exactly how - * decimals periods are handled with start_period. - */ - const _startPeriod = Math.trunc(toNumber(startPeriod, this.locale)); - const _endPeriod = Math.trunc(toNumber(endPeriod, this.locale)); - const _factor = toNumber(factor, this.locale); - const _noSwitch = toBoolean(noSwitch); - assertCostPositiveOrZero(_cost); - assertSalvagePositiveOrZero(_salvage); - assertStartAndEndPeriodAreValid(_startPeriod, _endPeriod, _life); - assertDeprecationFactorStrictlyPositive(_factor); - if (_cost === 0) - return 0; - if (_salvage >= _cost) { - return _startPeriod < 1 ? _cost - _salvage : 0; - } - const doubleDeprecFactor = _factor / _life; - if (doubleDeprecFactor >= 1) { - return _startPeriod < 1 ? _cost - _salvage : 0; - } - let previousCost = _cost; - let currentDeprec = 0; - let resultDeprec = 0; - let isLinearDeprec = false; - for (let i = 0; i < _endPeriod; i++) { - // compute the current deprecation, or keep the last one if we reached a stage of linear deprecation - if (!isLinearDeprec || _noSwitch) { - const doubleDeprec = previousCost * doubleDeprecFactor; - const remainingPeriods = _life - i; - const linearDeprec = (previousCost - _salvage) / remainingPeriods; - if (!_noSwitch && linearDeprec > doubleDeprec) { - isLinearDeprec = true; - currentDeprec = linearDeprec; - } - else { - currentDeprec = doubleDeprec; - } - } - const nextCost = Math.max(previousCost - currentDeprec, _salvage); - if (i >= _startPeriod) { - resultDeprec += previousCost - nextCost; - } - previousCost = nextCost; - } - return resultDeprec; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // XIRR - // ----------------------------------------------------------------------------- - const XIRR = { - description: _t("Internal rate of return given non-periodic cash flows."), - args: [ - arg("cashflow_amounts (range)", _t("An range containing the income or payments associated with the investment.")), - arg("cashflow_dates (range)", _t("An range with dates corresponding to the cash flows in cashflow_amounts.")), - arg(`rate_guess (number, default=${RATE_GUESS_DEFAULT})`, _t("An estimate for what the internal rate of return will be.")), - ], - returns: ["NUMBER"], - compute: function (cashflowAmounts, cashflowDates, rateGuess = RATE_GUESS_DEFAULT) { - rateGuess = rateGuess || 0; - const guess = toNumber(rateGuess, this.locale); - const _cashFlows = cashflowAmounts.flat().map((val) => toNumber(val, this.locale)); - const _dates = cashflowDates.flat().map((val) => toNumber(val, this.locale)); - assertCashFlowsAndDatesHaveSameDimension(cashflowAmounts, cashflowDates); - assertCashFlowsHavePositiveAndNegativesValues(_cashFlows); - assertEveryDateGreaterThanFirstDateOfCashFlowDates(_dates); - assertRateGuessStrictlyGreaterThanMinusOne(guess); - const map = new Map(); - for (const i of range(0, _dates.length)) { - const date = _dates[i]; - if (map.has(date)) - map.set(date, map.get(date) + _cashFlows[i]); - else - map.set(date, _cashFlows[i]); - } - const dates = Array.from(map.keys()); - const values = dates.map((date) => map.get(date)); - /** - * https://support.microsoft.com/en-us/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d - * - * The rate is computed iteratively by trying to solve the equation - * - * - * 0 = SUM [ P_i * (1 + rate) ^((d_0 - d_i) / 365) ] + P_0 - * i = 1 => n - * - * with P_i = price number i - * d_i = date number i - * - * This function is not defined for rate < -1. For the case where we get rates < -1 in the Newton method, add - * a fallback for a number very close to -1 to continue the Newton method. - * - */ - const func = (rate) => { - let value = values[0]; - for (const i of range(1, values.length)) { - const dateDiff = (dates[0] - dates[i]) / 365; - value += values[i] * (1 + rate) ** dateDiff; - } - return value; - }; - const derivFunc = (rate) => { - let deriv = 0; - for (const i of range(1, values.length)) { - const dateDiff = (dates[0] - dates[i]) / 365; - deriv += dateDiff * values[i] * (1 + rate) ** (dateDiff - 1); - } - return deriv; - }; - const nanFallback = (previousFallback) => { - // -0.9 => -0.99 => -0.999 => ... - if (!previousFallback) - return -0.9; - return previousFallback / 10 - 0.9; - }; - return newtonMethod(func, derivFunc, guess, 40, 1e-5, nanFallback); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // XNPV - // ----------------------------------------------------------------------------- - const XNPV = { - description: _t("Net present value given to non-periodic cash flows.."), - args: [ - arg("discount (number)", _t("The discount rate of the investment over one period.")), - arg("cashflow_amounts (number, range)", _t("An range containing the income or payments associated with the investment.")), - arg("cashflow_dates (number, range)", _t("An range with dates corresponding to the cash flows in cashflow_amounts.")), - ], - returns: ["NUMBER"], - compute: function (discount, cashflowAmounts, cashflowDates) { - const rate = toNumber(discount, this.locale); - const _cashFlows = isMatrix(cashflowAmounts) - ? cashflowAmounts.flat().map((val) => strictToNumber(val, this.locale)) - : [strictToNumber(cashflowAmounts, this.locale)]; - const _dates = isMatrix(cashflowDates) - ? cashflowDates.flat().map((val) => strictToNumber(val, this.locale)) - : [strictToNumber(cashflowDates, this.locale)]; - if (isMatrix(cashflowDates) && isMatrix(cashflowAmounts)) { - assertCashFlowsAndDatesHaveSameDimension(cashflowAmounts, cashflowDates); - } - else { - assert(() => _cashFlows.length === _dates.length, _t("There must be the same number of values in cashflow_amounts and cashflow_dates.")); - } - assertEveryDateGreaterThanFirstDateOfCashFlowDates(_dates); - assertRateStrictlyPositive(rate); - if (_cashFlows.length === 1) - return _cashFlows[0]; - // aggregate values of the same date - const map = new Map(); - for (const i of range(0, _dates.length)) { - const date = _dates[i]; - if (map.has(date)) - map.set(date, map.get(date) + _cashFlows[i]); - else - map.set(date, _cashFlows[i]); - } - const dates = Array.from(map.keys()); - const values = dates.map((date) => map.get(date)); - /** - * https://support.microsoft.com/en-us/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d - * - * The present value is computed using - * - * - * NPV = SUM [ P_i *(1 + rate) ^((d_0 - d_i) / 365) ] + P_0 - * i = 1 => n - * - * with P_i = price number i - * d_i = date number i - * - * - */ - let pv = values[0]; - for (const i of range(1, values.length)) { - const dateDiff = (dates[0] - dates[i]) / 365; - pv += values[i] * (1 + rate) ** dateDiff; - } - return pv; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // YIELD - // ----------------------------------------------------------------------------- - const YIELD = { - description: _t("Annual yield of a security paying periodic interest."), - args: [ - arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), - arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), - arg("rate (number)", _t("The annualized rate of interest.")), - arg("price (number)", _t("The price at which the security is bought per 100 face value.")), - arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")), - arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4).")), - arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), - ], - returns: ["NUMBER"], - compute: function (settlement, maturity, rate, price, redemption, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const _settlement = Math.trunc(toNumber(settlement, this.locale)); - const _maturity = Math.trunc(toNumber(maturity, this.locale)); - const _rate = toNumber(rate, this.locale); - const _price = toNumber(price, this.locale); - const _redemption = toNumber(redemption, this.locale); - const _frequency = Math.trunc(toNumber(frequency, this.locale)); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); - assertCouponFrequencyIsValid(_frequency); - assertDayCountConventionIsValid(_dayCountConvention); - assert(() => _rate >= 0, _t("The rate (%s) must be positive or null.", _rate.toString())); - assertPriceStrictlyPositive(_price); - assertRedemptionStrictlyPositive(_redemption); - const years = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention); - const nbrRealCoupons = years * _frequency; - const nbrFullCoupons = Math.ceil(nbrRealCoupons); - const timeFirstCoupon = nbrRealCoupons - Math.floor(nbrRealCoupons) || 1; - const cashFlowFromCoupon = (100 * _rate) / _frequency; - if (nbrFullCoupons === 1) { - const subPart = _price + cashFlowFromCoupon * (1 - timeFirstCoupon); - return (((_redemption + cashFlowFromCoupon - subPart) * _frequency * (1 / timeFirstCoupon)) / - subPart); - } - // The result of YIELD function is the yield at which the PRICE function will return the given price. - // This algorithm uses the Newton's method on the PRICE function to determine the result. - // Newton's method: https://en.wikipedia.org/wiki/Newton%27s_method - // As the PRICE function isn't continuous, we apply the Newton's method on the numerator of the PRICE formula. - // For simplicity, it is not yield but yieldFactorPerPeriod (= 1 + yield / frequency) which will be calibrated in Newton's method. - // yield can be deduced from yieldFactorPerPeriod in sequence. - function priceNumerator(price, timeFirstCoupon, nbrFullCoupons, yieldFactorPerPeriod, cashFlowFromCoupon, redemption) { - let result = redemption - - (price + cashFlowFromCoupon * (1 - timeFirstCoupon)) * - yieldFactorPerPeriod ** (nbrFullCoupons - 1 + timeFirstCoupon); - for (let i = 1; i <= nbrFullCoupons; i++) { - result += cashFlowFromCoupon * yieldFactorPerPeriod ** (i - 1); - } - return result; - } - function priceNumeratorDeriv(price, timeFirstCoupon, nbrFullCoupons, yieldFactorPerPeriod, cashFlowFromCoupon) { - let result = -(price + cashFlowFromCoupon * (1 - timeFirstCoupon)) * - (nbrFullCoupons - 1 + timeFirstCoupon) * - yieldFactorPerPeriod ** (nbrFullCoupons - 2 + timeFirstCoupon); - for (let i = 1; i <= nbrFullCoupons; i++) { - result += cashFlowFromCoupon * (i - 1) * yieldFactorPerPeriod ** (i - 2); - } - return result; - } - function func(x) { - return priceNumerator(_price, timeFirstCoupon, nbrFullCoupons, x, cashFlowFromCoupon, _redemption); - } - function derivFunc(x) { - return priceNumeratorDeriv(_price, timeFirstCoupon, nbrFullCoupons, x, cashFlowFromCoupon); - } - const initYield = _rate + 1; - const initYieldFactorPerPeriod = 1 + initYield / _frequency; - const methodResult = newtonMethod(func, derivFunc, initYieldFactorPerPeriod, 100, 1e-5); - return (methodResult - 1) * _frequency; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // YIELDDISC - // ----------------------------------------------------------------------------- - const YIELDDISC = { - description: _t("Annual yield of a discount security."), - args: [ - arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), - arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), - arg("price (number)", _t("The price at which the security is bought per 100 face value.")), - arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")), - arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), - ], - returns: ["NUMBER"], - compute: function (settlement, maturity, price, redemption, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const _settlement = Math.trunc(toNumber(settlement, this.locale)); - const _maturity = Math.trunc(toNumber(maturity, this.locale)); - const _price = toNumber(price, this.locale); - const _redemption = toNumber(redemption, this.locale); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); - assertDayCountConventionIsValid(_dayCountConvention); - assertPriceStrictlyPositive(_price); - assertRedemptionStrictlyPositive(_redemption); - /** - * https://wiki.documentfoundation.org/Documentation/Calc_Functions/YIELDDISC - * - * (redemption / price) - 1 - * YIELDDISC = _____________________________________ - * YEARFRAC(settlement, maturity, basis) - */ - const yearFrac = YEARFRAC.compute.bind(this)(settlement, maturity, dayCountConvention); - return (_redemption / _price - 1) / yearFrac; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // YIELDMAT - // ----------------------------------------------------------------------------- - const YIELDMAT = { - description: _t("Annual yield of a security paying interest at maturity."), - args: [ - arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), - arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), - arg("issue (date)", _t("The date the security was initially issued.")), - arg("rate (number)", _t("The annualized rate of interest.")), - arg("price (number)", _t("The price at which the security is bought.")), - arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), - ], - returns: ["NUMBER"], - compute: function (settlement, maturity, issue, rate, price, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { - dayCountConvention = dayCountConvention || 0; - const _settlement = Math.trunc(toNumber(settlement, this.locale)); - const _maturity = Math.trunc(toNumber(maturity, this.locale)); - const _issue = Math.trunc(toNumber(issue, this.locale)); - const _rate = toNumber(rate, this.locale); - const _price = toNumber(price, this.locale); - const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); - assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); - assertDayCountConventionIsValid(_dayCountConvention); - assert(() => _settlement >= _issue, _t("The settlement (%s) must be greater than or equal to the issue (%s).", _settlement.toString(), _issue.toString())); - assert(() => _rate >= 0, _t("The rate (%s) must be positive or null.", _rate.toString())); - assertPriceStrictlyPositive(_price); - const issueToMaturity = YEARFRAC.compute.bind(this)(_issue, _maturity, _dayCountConvention); - const issueToSettlement = YEARFRAC.compute.bind(this)(_issue, _settlement, _dayCountConvention); - const settlementToMaturity = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention); - const numerator = (100 * (1 + _rate * issueToMaturity)) / (_price + 100 * _rate * issueToSettlement) - 1; - return numerator / settlementToMaturity; - }, - isExported: true, - }; - - var financial = /*#__PURE__*/Object.freeze({ - __proto__: null, - ACCRINTM: ACCRINTM, - AMORLINC: AMORLINC, - COUPDAYBS: COUPDAYBS, - COUPDAYS: COUPDAYS, - COUPDAYSNC: COUPDAYSNC, - COUPNCD: COUPNCD, - COUPNUM: COUPNUM, - COUPPCD: COUPPCD, - CUMIPMT: CUMIPMT, - CUMPRINC: CUMPRINC, - DB: DB, - DDB: DDB, - DISC: DISC, - DOLLARDE: DOLLARDE, - DOLLARFR: DOLLARFR, - DURATION: DURATION, - EFFECT: EFFECT, - FV: FV, - FVSCHEDULE: FVSCHEDULE, - INTRATE: INTRATE, - IPMT: IPMT, - IRR: IRR, - ISPMT: ISPMT, - MDURATION: MDURATION, - MIRR: MIRR, - NOMINAL: NOMINAL, - NPER: NPER, - NPV: NPV, - PDURATION: PDURATION, - PMT: PMT, - PPMT: PPMT, - PRICE: PRICE, - PRICEDISC: PRICEDISC, - PRICEMAT: PRICEMAT, - PV: PV, - RATE: RATE, - RECEIVED: RECEIVED, - RRI: RRI, - SLN: SLN, - SYD: SYD, - TBILLEQ: TBILLEQ, - TBILLPRICE: TBILLPRICE, - TBILLYIELD: TBILLYIELD, - VDB: VDB, - XIRR: XIRR, - XNPV: XNPV, - YIELD: YIELD, - YIELDDISC: YIELDDISC, - YIELDMAT: YIELDMAT - }); - - // ----------------------------------------------------------------------------- - // ISERR - // ----------------------------------------------------------------------------- - const ISERR = { - description: _t("Whether a value is an error other than #N/A."), - args: [arg("value (any, lazy)", _t("The value to be verified as an error type."))], - returns: ["BOOLEAN"], - compute: function (value) { - try { - value(); - return false; - } - catch (e) { - return e?.errorType != CellErrorType.NotAvailable; - } - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ISERROR - // ----------------------------------------------------------------------------- - const ISERROR = { - description: _t("Whether a value is an error."), - args: [arg("value (any, lazy)", _t("The value to be verified as an error type."))], - returns: ["BOOLEAN"], - compute: function (value) { - try { - value(); - return false; - } - catch (e) { - return true; - } - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ISLOGICAL - // ----------------------------------------------------------------------------- - const ISLOGICAL = { - description: _t("Whether a value is `true` or `false`."), - args: [arg("value (any, lazy)", _t("The value to be verified as a logical TRUE or FALSE."))], - returns: ["BOOLEAN"], - compute: function (value) { - try { - return typeof value() === "boolean"; - } - catch (e) { - return false; - } - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ISNA - // ----------------------------------------------------------------------------- - const ISNA = { - description: _t("Whether a value is the error #N/A."), - args: [arg("value (any, lazy)", _t("The value to be verified as an error type."))], - returns: ["BOOLEAN"], - compute: function (value) { - try { - value(); - return false; - } - catch (e) { - return e?.errorType === CellErrorType.NotAvailable; - } - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ISNONTEXT - // ----------------------------------------------------------------------------- - const ISNONTEXT = { - description: _t("Whether a value is non-textual."), - args: [arg("value (any, lazy)", _t("The value to be checked."))], - returns: ["BOOLEAN"], - compute: function (value) { - try { - return typeof value() !== "string"; - } - catch (e) { - return true; - } - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ISNUMBER - // ----------------------------------------------------------------------------- - const ISNUMBER = { - description: _t("Whether a value is a number."), - args: [arg("value (any, lazy)", _t("The value to be verified as a number."))], - returns: ["BOOLEAN"], - compute: function (value) { - try { - return typeof value() === "number"; - } - catch (e) { - return false; - } - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ISTEXT - // ----------------------------------------------------------------------------- - const ISTEXT = { - description: _t("Whether a value is text."), - args: [arg("value (any, lazy)", _t("The value to be verified as text."))], - returns: ["BOOLEAN"], - compute: function (value) { - try { - return typeof value() === "string"; - } - catch (e) { - return false; - } - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ISBLANK - // ----------------------------------------------------------------------------- - const ISBLANK = { - description: _t("Whether the referenced cell is empty"), - args: [arg("value (any, lazy)", _t("Reference to the cell that will be checked for emptiness."))], - returns: ["BOOLEAN"], - compute: function (value) { - try { - const val = value(); - return val === null; - } - catch (e) { - return false; - } - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // NA - // ----------------------------------------------------------------------------- - const NA = { - description: _t("Returns the error value #N/A."), - args: [], - returns: ["BOOLEAN"], - compute: function (value) { - throw new NotAvailableError(); - }, - isExported: true, - }; - - var info = /*#__PURE__*/Object.freeze({ - __proto__: null, - ISBLANK: ISBLANK, - ISERR: ISERR, - ISERROR: ISERROR, - ISLOGICAL: ISLOGICAL, - ISNA: ISNA, - ISNONTEXT: ISNONTEXT, - ISNUMBER: ISNUMBER, - ISTEXT: ISTEXT, - NA: NA - }); - - // ----------------------------------------------------------------------------- - // AND - // ----------------------------------------------------------------------------- - const AND = { - description: _t("Logical `and` operator."), - args: [ - arg("logical_expression1 (boolean, range)", _t("An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE, or an expression that can be coerced to a logical value.")), - arg("logical_expression2 (boolean, range, repeating)", _t("More expressions that represent logical values.")), - ], - returns: ["BOOLEAN"], - compute: function (...logicalExpressions) { - let foundBoolean = false; - let acc = true; - conditionalVisitBoolean(logicalExpressions, (arg) => { - foundBoolean = true; - acc = acc && arg; - return acc; - }); - assert(() => foundBoolean, _t(`[[FUNCTION_NAME]] has no valid input data.`)); - return acc; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // FALSE - // ----------------------------------------------------------------------------- - const FALSE = { - description: _t("Logical value `false`."), - args: [], - returns: ["BOOLEAN"], - compute: function () { - return false; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // IF - // ----------------------------------------------------------------------------- - const IF = { - description: _t("Returns value depending on logical expression."), - args: [ - arg("logical_expression (boolean)", _t("An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE.")), - arg("value_if_true (any, lazy)", _t("The value the function returns if logical_expression is TRUE.")), - arg("value_if_false (any, lazy, default=FALSE)", _t("The value the function returns if logical_expression is FALSE.")), - ], - returns: ["ANY"], - computeValueAndFormat: function (logicalExpression, valueIfTrue, valueIfFalse = () => ({ value: false })) { - const result = toBoolean(logicalExpression?.value) ? valueIfTrue() : valueIfFalse(); - if (result === undefined) { - return { value: "" }; - } - if (result.value === null) { - result.value = ""; - } - return result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // IFERROR - // ----------------------------------------------------------------------------- - const IFERROR = { - description: _t("Value if it is not an error, otherwise 2nd argument."), - args: [ - arg("value (any, lazy)", _t("The value to return if value itself is not an error.")), - arg(`value_if_error (any, lazy, default="empty")`, _t("The value the function returns if value is an error.")), - ], - returns: ["ANY"], - computeValueAndFormat: function (value, valueIfError = () => ({ value: "" })) { - let result; - try { - result = value(); - } - catch (e) { - result = valueIfError(); - } - if (result === undefined) { - return { value: "" }; - } - if (result.value === null) { - result.value = ""; - } - return result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // IFNA - // ----------------------------------------------------------------------------- - const IFNA = { - description: _t("Value if it is not an #N/A error, otherwise 2nd argument."), - args: [ - arg("value (any, lazy)", _t("The value to return if value itself is not #N/A an error.")), - arg(`value_if_error (any, lazy, default="empty")`, _t("The value the function returns if value is an #N/A error.")), - ], - returns: ["ANY"], - computeValueAndFormat: function (value, valueIfError = () => ({ value: "" })) { - let result; - try { - result = value(); - } - catch (e) { - if (e.errorType === CellErrorType.NotAvailable) { - result = valueIfError(); - } - else { - result = value(); - } - } - if (result === undefined) { - return { value: "" }; - } - if (result.value === null) { - result.value = ""; - } - return result; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // IFS - // ----------------------------------------------------------------------------- - const IFS = { - description: _t("Returns a value depending on multiple logical expressions."), - args: [ - arg("condition1 (boolean, lazy)", _t("The first condition to be evaluated. This can be a boolean, a number, an array, or a reference to any of those.")), - arg("value1 (any, lazy)", _t("The returned value if condition1 is TRUE.")), - arg("condition2 (boolean, lazy, repeating)", _t("Additional conditions to be evaluated if the previous ones are FALSE.")), - arg("value2 (any, lazy, repeating)", _t("Additional values to be returned if their corresponding conditions are TRUE.")), - ], - returns: ["ANY"], - computeValueAndFormat: function (...values) { - assert(() => values.length % 2 === 0, _t(`Wrong number of arguments. Expected an even number of arguments.`)); - for (let n = 0; n < values.length - 1; n += 2) { - if (toBoolean(values[n]()?.value)) { - const result = values[n + 1](); - if (result === undefined) { - return { value: "" }; - } - if (result.value === null) { - result.value = ""; - } - return result; - } - } - throw new Error(_t(`No match.`)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // NOT - // ----------------------------------------------------------------------------- - const NOT = { - description: _t("Returns opposite of provided logical value."), - args: [ - arg("logical_expression (boolean)", _t("An expression or reference to a cell holding an expression that represents some logical value.")), - ], - returns: ["BOOLEAN"], - compute: function (logicalExpression) { - return !toBoolean(logicalExpression); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // OR - // ----------------------------------------------------------------------------- - const OR = { - description: _t("Logical `or` operator."), - args: [ - arg("logical_expression1 (boolean, range)", _t("An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE, or an expression that can be coerced to a logical value.")), - arg("logical_expression2 (boolean, range, repeating)", _t("More expressions that evaluate to logical values.")), - ], - returns: ["BOOLEAN"], - compute: function (...logicalExpressions) { - let foundBoolean = false; - let acc = false; - conditionalVisitBoolean(logicalExpressions, (arg) => { - foundBoolean = true; - acc = acc || arg; - return !acc; - }); - assert(() => foundBoolean, _t(`[[FUNCTION_NAME]] has no valid input data.`)); - return acc; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TRUE - // ----------------------------------------------------------------------------- - const TRUE = { - description: _t("Logical value `true`."), - args: [], - returns: ["BOOLEAN"], - compute: function () { - return true; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // XOR - // ----------------------------------------------------------------------------- - const XOR = { - description: _t("Logical `xor` operator."), - args: [ - arg("logical_expression1 (boolean, range)", _t("An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE, or an expression that can be coerced to a logical value.")), - arg("logical_expression2 (boolean, range, repeating)", _t("More expressions that evaluate to logical values.")), - ], - returns: ["BOOLEAN"], - compute: function (...logicalExpressions) { - let foundBoolean = false; - let acc = false; - conditionalVisitBoolean(logicalExpressions, (arg) => { - foundBoolean = true; - acc = acc ? !arg : arg; - return true; // no stop condition - }); - assert(() => foundBoolean, _t(`[[FUNCTION_NAME]] has no valid input data.`)); - return acc; - }, - isExported: true, - }; - - var logical = /*#__PURE__*/Object.freeze({ - __proto__: null, - AND: AND, - FALSE: FALSE, - IF: IF, - IFERROR: IFERROR, - IFNA: IFNA, - IFS: IFS, - NOT: NOT, - OR: OR, - TRUE: TRUE, - XOR: XOR - }); - - const DEFAULT_IS_SORTED = true; - const DEFAULT_MATCH_MODE = 0; - const DEFAULT_SEARCH_MODE = 1; - const DEFAULT_ABSOLUTE_RELATIVE_MODE = 1; - function assertAvailable(variable, searchKey) { - if (variable === undefined) { - throw new NotAvailableError(_t("Did not find value '%s' in [[FUNCTION_NAME]] evaluation.", toString(searchKey))); - } - } - // ----------------------------------------------------------------------------- - // ADDRESS - // ----------------------------------------------------------------------------- - const ADDRESS = { - description: _t("Returns a cell reference as a string. "), - args: [ - arg("row (number)", _t("The row number of the cell reference. ")), - arg("column (number)", _t("The column number (not name) of the cell reference. A is column number 1. ")), - arg(`absolute_relative_mode (number, default=${DEFAULT_ABSOLUTE_RELATIVE_MODE})`, _t("An indicator of whether the reference is row/column absolute. 1 is row and column absolute (e.g. $A$1), 2 is row absolute and column relative (e.g. A$1), 3 is row relative and column absolute (e.g. $A1), and 4 is row and column relative (e.g. A1).")), - arg("use_a1_notation (boolean, default=TRUE)", _t("A boolean indicating whether to use A1 style notation (TRUE) or R1C1 style notation (FALSE).")), - arg("sheet (string, optional)", _t("A string indicating the name of the sheet into which the address points.")), - ], - returns: ["STRING"], - compute: function (row, column, absoluteRelativeMode = DEFAULT_ABSOLUTE_RELATIVE_MODE, useA1Notation = true, sheet) { - const rowNumber = strictToInteger(row, this.locale); - const colNumber = strictToInteger(column, this.locale); - assertNumberGreaterThanOrEqualToOne(rowNumber); - assertNumberGreaterThanOrEqualToOne(colNumber); - const _absoluteRelativeMode = strictToInteger(absoluteRelativeMode, this.locale); - assert(() => [1, 2, 3, 4].includes(_absoluteRelativeMode), expectNumberRangeError(1, 4, _absoluteRelativeMode)); - const _useA1Notation = toBoolean(useA1Notation); - let cellReference; - if (_useA1Notation) { - const rangePart = { - rowFixed: [1, 2].includes(_absoluteRelativeMode) ? true : false, - colFixed: [1, 3].includes(_absoluteRelativeMode) ? true : false, - }; - cellReference = toXC(colNumber - 1, rowNumber - 1, rangePart); - } - else { - const rowPart = [1, 2].includes(_absoluteRelativeMode) ? `R${rowNumber}` : `R[${rowNumber}]`; - const colPart = [1, 3].includes(_absoluteRelativeMode) ? `C${colNumber}` : `C[${colNumber}]`; - cellReference = rowPart + colPart; - } - if (sheet !== undefined) { - return `${getCanonicalSheetName(toString(sheet))}!${cellReference}`; - } - return cellReference; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COLUMN - // ----------------------------------------------------------------------------- - const COLUMN = { - description: _t("Column number of a specified cell."), - args: [ - arg("cell_reference (meta, default='this cell')", _t("The cell whose column number will be returned. Column A corresponds to 1. By default, the function use the cell in which the formula is entered.")), - ], - returns: ["NUMBER"], - compute: function (cellReference) { - const _cellReference = cellReference || this.__originCellXC?.(); - assert(() => !!_cellReference, "In this context, the function [[FUNCTION_NAME]] needs to have a cell or range in parameter."); - const zone = toZone(_cellReference); - return zone.left + 1; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // COLUMNS - // ----------------------------------------------------------------------------- - const COLUMNS = { - description: _t("Number of columns in a specified array or range."), - args: [arg("range (meta)", _t("The range whose column count will be returned."))], - returns: ["NUMBER"], - compute: function (range) { - const zone = toZone(range); - return zone.right - zone.left + 1; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // HLOOKUP - // ----------------------------------------------------------------------------- - const HLOOKUP = { - description: _t(`Horizontal lookup`), - args: [ - arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")), - arg("range (range)", _t("The range to consider for the search. The first row in the range is searched for the key specified in search_key.")), - arg("index (number)", _t("The row index of the value to be returned, where the first row in range is numbered 1.")), - arg(`is_sorted (boolean, default=${DEFAULT_IS_SORTED})`, _t("Indicates whether the row to be searched (the first row of the specified range) is sorted, in which case the closest match for search_key will be returned.")), - ], - returns: ["ANY"], - computeValueAndFormat: function (searchKey, range, index, isSorted = { value: DEFAULT_IS_SORTED }) { - const _index = Math.trunc(toNumber(index?.value, this.locale)); - assert(() => 1 <= _index && _index <= range[0].length, _t("[[FUNCTION_NAME]] evaluates to an out of bounds range.")); - const getValueFromRange = (range, index) => range[index][0].value; - const _isSorted = toBoolean(isSorted.value); - const colIndex = _isSorted - ? dichotomicSearch(range, searchKey?.value, "nextSmaller", "asc", range.length, getValueFromRange) - : linearSearch(range, searchKey?.value, "strict", range.length, getValueFromRange); - const col = range[colIndex]; - assertAvailable(col, searchKey?.value); - return col[_index - 1]; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // INDEX - // ----------------------------------------------------------------------------- - const INDEX = { - description: _t("Returns the content of a cell, specified by row and column offset."), - args: [ - arg("reference (any, range)", _t("The range of cells from which the values are returned.")), - arg("row (number, default=0)", _t("The index of the row to be returned from within the reference range of cells.")), - arg("column (number, default=0)", _t("The index of the column to be returned from within the reference range of cells.")), - ], - returns: ["ANY"], - computeValueAndFormat: function (reference, row = { value: 0 }, column = { value: 0 }) { - const _reference = isMatrix(reference) ? reference : [[reference]]; - const _row = toNumber(row.value, this.locale); - const _column = toNumber(column.value, this.locale); - assert(() => _column >= 0 && - _column - 1 < _reference.length && - _row >= 0 && - _row - 1 < _reference[0].length, _t("Index out of range.")); - if (_row === 0 && _column === 0) { - return _reference; - } - if (_row === 0) { - return [_reference[_column - 1]]; - } - if (_column === 0) { - return _reference.map((col) => [col[_row - 1]]); - } - return _reference[_column - 1][_row - 1]; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // LOOKUP - // ----------------------------------------------------------------------------- - const LOOKUP = { - description: _t(`Look up a value.`), - args: [ - arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")), - arg("search_array (range)", _t("One method of using this function is to provide a single sorted row or column search_array to look through for the search_key with a second argument result_range. The other way is to combine these two arguments into one search_array where the first row or column is searched and a value is returned from the last row or column in the array. If search_key is not found, a non-exact match may be returned.")), - arg("result_range (range, optional)", _t("The range from which to return a result. The value returned corresponds to the location where search_key is found in search_range. This range must be only a single row or column and should not be used if using the search_result_array method.")), - ], - returns: ["ANY"], - computeValueAndFormat: function (searchKey, searchArray, resultRange) { - let nbCol = searchArray.length; - let nbRow = searchArray[0].length; - const verticalSearch = nbRow >= nbCol; - const getElement = verticalSearch - ? (range, index) => range[0][index].value - : (range, index) => range[index][0].value; - const rangeLength = verticalSearch ? nbRow : nbCol; - const index = dichotomicSearch(searchArray, searchKey?.value, "nextSmaller", "asc", rangeLength, getElement); - if (index === -1) - assertAvailable(undefined, searchKey?.value); - verticalSearch - ? assertAvailable(searchArray[0][index], searchKey?.value) - : assertAvailable(searchArray[index][nbRow - 1], searchKey?.value); - if (resultRange === undefined) { - return verticalSearch ? searchArray[nbCol - 1][index] : searchArray[index][nbRow - 1]; - } - nbCol = resultRange.length; - nbRow = resultRange[0].length; - assert(() => nbCol === 1 || nbRow === 1, _t("The result_range must be a single row or a single column.")); - if (nbCol > 1) { - assert(() => index <= nbCol - 1, _t("[[FUNCTION_NAME]] evaluates to an out of range row value %s.", (index + 1).toString())); - return resultRange[index][0]; - } - assert(() => index <= nbRow - 1, _t("[[FUNCTION_NAME]] evaluates to an out of range column value %s.", (index + 1).toString())); - return resultRange[0][index]; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MATCH - // ----------------------------------------------------------------------------- - const DEFAULT_SEARCH_TYPE = 1; - const MATCH = { - description: _t(`Position of item in range that matches value.`), - args: [ - arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")), - arg("range (any, range)", _t("The one-dimensional array to be searched.")), - arg(`search_type (number, default=${DEFAULT_SEARCH_TYPE})`, _t("The search method. 1 (default) finds the largest value less than or equal to search_key when range is sorted in ascending order. 0 finds the exact value when range is unsorted. -1 finds the smallest value greater than or equal to search_key when range is sorted in descending order.")), - ], - returns: ["NUMBER"], - compute: function (searchKey, range, searchType = DEFAULT_SEARCH_TYPE) { - let _searchType = toNumber(searchType, this.locale); - const nbCol = range.length; - const nbRow = range[0].length; - assert(() => nbCol === 1 || nbRow === 1, _t("The range must be a single row or a single column.")); - let index = -1; - const getElement = nbCol === 1 - ? (range, index) => range[0][index] - : (range, index) => range[index][0]; - const rangeLen = nbCol === 1 ? range[0].length : range.length; - _searchType = Math.sign(_searchType); - switch (_searchType) { - case 1: - index = dichotomicSearch(range, searchKey, "nextSmaller", "asc", rangeLen, getElement); - break; - case 0: - index = linearSearch(range, searchKey, "strict", rangeLen, getElement); - break; - case -1: - index = dichotomicSearch(range, searchKey, "nextGreater", "desc", rangeLen, getElement); - break; - } - assertAvailable(nbCol === 1 ? range[0][index] : range[index], searchKey); - return index + 1; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ROW - // ----------------------------------------------------------------------------- - const ROW = { - description: _t("Row number of a specified cell."), - args: [ - arg("cell_reference (meta, default='this cell')", _t("The cell whose row number will be returned. By default, this function uses the cell in which the formula is entered.")), - ], - returns: ["NUMBER"], - compute: function (cellReference) { - cellReference = cellReference || this.__originCellXC?.(); - assert(() => !!cellReference, "In this context, the function [[FUNCTION_NAME]] needs to have a cell or range in parameter."); - const zone = toZone(cellReference); - return zone.top + 1; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // ROWS - // ----------------------------------------------------------------------------- - const ROWS = { - description: _t("Number of rows in a specified array or range."), - args: [arg("range (meta)", _t("The range whose row count will be returned."))], - returns: ["NUMBER"], - compute: function (range) { - const zone = toZone(range); - return zone.bottom - zone.top + 1; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // VLOOKUP - // ----------------------------------------------------------------------------- - const VLOOKUP = { - description: _t(`Vertical lookup.`), - args: [ - arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")), - arg("range (any, range)", _t("The range to consider for the search. The first column in the range is searched for the key specified in search_key.")), - arg("index (number)", _t("The column index of the value to be returned, where the first column in range is numbered 1.")), - arg(`is_sorted (boolean, default=${DEFAULT_IS_SORTED})`, _t("Indicates whether the column to be searched (the first column of the specified range) is sorted, in which case the closest match for search_key will be returned.")), - ], - returns: ["ANY"], - computeValueAndFormat: function (searchKey, range, index, isSorted = { value: DEFAULT_IS_SORTED }) { - const _index = Math.trunc(toNumber(index?.value, this.locale)); - assert(() => 1 <= _index && _index <= range.length, _t("[[FUNCTION_NAME]] evaluates to an out of bounds range.")); - const getValueFromRange = (range, index) => range[0][index].value; - const _isSorted = toBoolean(isSorted.value); - const rowIndex = _isSorted - ? dichotomicSearch(range, searchKey?.value, "nextSmaller", "asc", range[0].length, getValueFromRange) - : linearSearch(range, searchKey?.value, "strict", range[0].length, getValueFromRange); - const value = range[_index - 1][rowIndex]; - assertAvailable(value, searchKey); - return value; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // XLOOKUP - // ----------------------------------------------------------------------------- - const XLOOKUP = { - description: _t(`Search a range for a match and return the corresponding item from a second range.`), - args: [ - arg("search_key (any)", _t("The value to search for.")), - arg("lookup_range (any, range)", _t("The range to consider for the search. Should be a single column or a single row.")), - arg("return_range (any, range)", _t("The range containing the return value. Should have the same dimensions as lookup_range.")), - arg("if_not_found (any, lazy, optional)", _t("If a valid match is not found, return this value.")), - arg(`match_mode (any, default=${DEFAULT_MATCH_MODE})`, _t("(0) Exact match. (-1) Return next smaller item if no match. (1) Return next greater item if no match.")), - arg(`search_mode (any, default=${DEFAULT_SEARCH_MODE})`, _t("(1) Search starting at first item. \ - (-1) Search starting at last item. \ - (2) Perform a binary search that relies on lookup_array being sorted in ascending order. If not sorted, invalid results will be returned. \ - (-2) Perform a binary search that relies on lookup_array being sorted in descending order. If not sorted, invalid results will be returned.\ - ")), - ], - returns: ["ANY"], - computeValueAndFormat: function (searchKey, lookupRange, returnRange, defaultValue, matchMode = { value: DEFAULT_MATCH_MODE }, searchMode = { value: DEFAULT_SEARCH_MODE }) { - const _matchMode = Math.trunc(toNumber(matchMode.value, this.locale)); - const _searchMode = Math.trunc(toNumber(searchMode.value, this.locale)); - assert(() => lookupRange.length === 1 || lookupRange[0].length === 1, _t("lookup_range should be either a single row or single column.")); - assert(() => [-1, 1, -2, 2].includes(_searchMode), _t("searchMode should be a value in [-1, 1, -2, 2].")); - assert(() => [-1, 0, 1].includes(_matchMode), _t("matchMode should be a value in [-1, 0, 1].")); - const lookupDirection = lookupRange.length === 1 ? "col" : "row"; - assert(() => lookupDirection === "col" - ? returnRange[0].length === lookupRange[0].length - : returnRange.length === lookupRange.length, _t("return_range should have the same dimensions as lookup_range.")); - const getElement = lookupDirection === "col" - ? (range, index) => range[0][index].value - : (range, index) => range[index][0].value; - const rangeLen = lookupDirection === "col" ? lookupRange[0].length : lookupRange.length; - const mode = _matchMode === 0 ? "strict" : _matchMode === 1 ? "nextGreater" : "nextSmaller"; - const reverseSearch = _searchMode === -1; - const index = _searchMode === 2 || _searchMode === -2 - ? dichotomicSearch(lookupRange, searchKey?.value, mode, _searchMode === 2 ? "asc" : "desc", rangeLen, getElement) - : linearSearch(lookupRange, searchKey?.value, mode, rangeLen, getElement, reverseSearch); - if (index !== -1) { - return lookupDirection === "col" - ? returnRange.map((col) => [col[index]]) - : [returnRange[index]]; - } - const _defaultValue = defaultValue?.(); - assertAvailable(_defaultValue, searchKey); - return [[_defaultValue]]; - }, - isExported: true, - }; - - var lookup = /*#__PURE__*/Object.freeze({ - __proto__: null, - ADDRESS: ADDRESS, - COLUMN: COLUMN, - COLUMNS: COLUMNS, - HLOOKUP: HLOOKUP, - INDEX: INDEX, - LOOKUP: LOOKUP, - MATCH: MATCH, - ROW: ROW, - ROWS: ROWS, - VLOOKUP: VLOOKUP, - XLOOKUP: XLOOKUP - }); - - // ----------------------------------------------------------------------------- - // ADD - // ----------------------------------------------------------------------------- - const ADD = { - description: _t(`Sum of two numbers.`), - args: [ - arg("value1 (number)", _t("The first addend.")), - arg("value2 (number)", _t("The second addend.")), - ], - returns: ["NUMBER"], - computeFormat: (value1, value2) => value1?.format || value2?.format, - compute: function (value1, value2) { - return toNumber(value1, this.locale) + toNumber(value2, this.locale); - }, - }; - // ----------------------------------------------------------------------------- - // CONCAT - // ----------------------------------------------------------------------------- - const CONCAT = { - description: _t(`Concatenation of two values.`), - args: [ - arg("value1 (string)", _t("The value to which value2 will be appended.")), - arg("value2 (string)", _t("The value to append to value1.")), - ], - returns: ["STRING"], - compute: function (value1, value2) { - return toString(value1) + toString(value2); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // DIVIDE - // ----------------------------------------------------------------------------- - const DIVIDE = { - description: _t(`One number divided by another.`), - args: [ - arg("dividend (number)", _t("The number to be divided.")), - arg("divisor (number)", _t("The number to divide by.")), - ], - returns: ["NUMBER"], - computeFormat: (dividend, divisor) => dividend?.format || divisor?.format, - compute: function (dividend, divisor) { - const _divisor = toNumber(divisor, this.locale); - assert(() => _divisor !== 0, _t("The divisor must be different from zero.")); - return toNumber(dividend, this.locale) / _divisor; - }, - }; - // ----------------------------------------------------------------------------- - // EQ - // ----------------------------------------------------------------------------- - function isEmpty(value) { - return value === null || value === undefined; - } - const getNeutral = { number: 0, string: "", boolean: false }; - const EQ = { - description: _t(`Equal.`), - args: [ - arg("value1 (any)", _t("The first value.")), - arg("value2 (any)", _t("The value to test against value1 for equality.")), - ], - returns: ["BOOLEAN"], - compute: function (value1, value2) { - value1 = isEmpty(value1) ? getNeutral[typeof value2] : value1; - value2 = isEmpty(value2) ? getNeutral[typeof value1] : value2; - if (typeof value1 === "string") { - value1 = value1.toUpperCase(); - } - if (typeof value2 === "string") { - value2 = value2.toUpperCase(); - } - return value1 === value2; - }, - }; - // ----------------------------------------------------------------------------- - // GT - // ----------------------------------------------------------------------------- - function applyRelationalOperator(value1, value2, cb) { - value1 = isEmpty(value1) ? getNeutral[typeof value2] : value1; - value2 = isEmpty(value2) ? getNeutral[typeof value1] : value2; - if (typeof value1 !== "number") { - value1 = toString(value1).toUpperCase(); - } - if (typeof value2 !== "number") { - value2 = toString(value2).toUpperCase(); - } - const tV1 = typeof value1; - const tV2 = typeof value2; - if (tV1 === "string" && tV2 === "number") { - return true; - } - if (tV2 === "string" && tV1 === "number") { - return false; - } - return cb(value1, value2); - } - const GT = { - description: _t(`Strictly greater than.`), - args: [ - arg("value1 (any)", _t("The value to test as being greater than value2.")), - arg("value2 (any)", _t("The second value.")), - ], - returns: ["BOOLEAN"], - compute: function (value1, value2) { - return applyRelationalOperator(value1, value2, (v1, v2) => { - return v1 > v2; - }); - }, - }; - // ----------------------------------------------------------------------------- - // GTE - // ----------------------------------------------------------------------------- - const GTE = { - description: _t(`Greater than or equal to.`), - args: [ - arg("value1 (any)", _t("The value to test as being greater than or equal to value2.")), - arg("value2 (any)", _t("The second value.")), - ], - returns: ["BOOLEAN"], - compute: function (value1, value2) { - return applyRelationalOperator(value1, value2, (v1, v2) => { - return v1 >= v2; - }); - }, - }; - // ----------------------------------------------------------------------------- - // LT - // ----------------------------------------------------------------------------- - const LT = { - description: _t(`Less than.`), - args: [ - arg("value1 (any)", _t("The value to test as being less than value2.")), - arg("value2 (any)", _t("The second value.")), - ], - returns: ["BOOLEAN"], - compute: function (value1, value2) { - return !GTE.compute.bind(this)(value1, value2); - }, - }; - // ----------------------------------------------------------------------------- - // LTE - // ----------------------------------------------------------------------------- - const LTE = { - description: _t(`Less than or equal to.`), - args: [ - arg("value1 (any)", _t("The value to test as being less than or equal to value2.")), - arg("value2 (any)", _t("The second value.")), - ], - returns: ["BOOLEAN"], - compute: function (value1, value2) { - return !GT.compute.bind(this)(value1, value2); - }, - }; - // ----------------------------------------------------------------------------- - // MINUS - // ----------------------------------------------------------------------------- - const MINUS = { - description: _t(`Difference of two numbers.`), - args: [ - arg("value1 (number)", _t("The minuend, or number to be subtracted from.")), - arg("value2 (number)", _t("The subtrahend, or number to subtract from value1.")), - ], - returns: ["NUMBER"], - computeFormat: (value1, value2) => value1?.format || value2?.format, - compute: function (value1, value2) { - return toNumber(value1, this.locale) - toNumber(value2, this.locale); - }, - }; - // ----------------------------------------------------------------------------- - // MULTIPLY - // ----------------------------------------------------------------------------- - const MULTIPLY = { - description: _t(`Product of two numbers`), - args: [ - arg("factor1 (number)", _t("The first multiplicand.")), - arg("factor2 (number)", _t("The second multiplicand.")), - ], - returns: ["NUMBER"], - computeFormat: (factor1, factor2) => factor1?.format || factor2?.format, - compute: function (factor1, factor2) { - return toNumber(factor1, this.locale) * toNumber(factor2, this.locale); - }, - }; - // ----------------------------------------------------------------------------- - // NE - // ----------------------------------------------------------------------------- - const NE = { - description: _t(`Not equal.`), - args: [ - arg("value1 (any)", _t("The first value.")), - arg("value2 (any)", _t("The value to test against value1 for inequality.")), - ], - returns: ["BOOLEAN"], - compute: function (value1, value2) { - return !EQ.compute.bind(this)(value1, value2); - }, - }; - // ----------------------------------------------------------------------------- - // POW - // ----------------------------------------------------------------------------- - const POW = { - description: _t(`A number raised to a power.`), - args: [ - arg("base (number)", _t("The number to raise to the exponent power.")), - arg("exponent (number)", _t("The exponent to raise base to.")), - ], - returns: ["NUMBER"], - compute: function (base, exponent) { - return POWER.compute.bind(this)(base, exponent); - }, - }; - // ----------------------------------------------------------------------------- - // UMINUS - // ----------------------------------------------------------------------------- - const UMINUS = { - description: _t(`A number with the sign reversed.`), - args: [ - arg("value (number)", _t("The number to have its sign reversed. Equivalently, the number to multiply by -1.")), - ], - computeFormat: (value) => value?.format, - returns: ["NUMBER"], - compute: function (value) { - return -toNumber(value, this.locale); - }, - }; - // ----------------------------------------------------------------------------- - // UNARY_PERCENT - // ----------------------------------------------------------------------------- - const UNARY_PERCENT = { - description: _t(`Value interpreted as a percentage.`), - args: [arg("percentage (number)", _t("The value to interpret as a percentage."))], - returns: ["NUMBER"], - compute: function (percentage) { - return toNumber(percentage, this.locale) / 100; - }, - }; - // ----------------------------------------------------------------------------- - // UPLUS - // ----------------------------------------------------------------------------- - const UPLUS = { - description: _t(`A specified number, unchanged.`), - args: [arg("value (any)", _t("The number to return."))], - returns: ["ANY"], - computeFormat: (value) => value?.format, - compute: function (value = "") { - return value === null ? "" : value; - }, - }; - - var operators = /*#__PURE__*/Object.freeze({ - __proto__: null, - ADD: ADD, - CONCAT: CONCAT, - DIVIDE: DIVIDE, - EQ: EQ, - GT: GT, - GTE: GTE, - LT: LT, - LTE: LTE, - MINUS: MINUS, - MULTIPLY: MULTIPLY, - NE: NE, - POW: POW, - UMINUS: UMINUS, - UNARY_PERCENT: UNARY_PERCENT, - UPLUS: UPLUS - }); - - const DEFAULT_STARTING_AT = 1; - /** Regex matching all the words in a string */ - const wordRegex = /[A-Za-zÀ-ÖØ-öø-ÿ]+/g; - // ----------------------------------------------------------------------------- - // CHAR - // ----------------------------------------------------------------------------- - const CHAR = { - description: _t("Gets character associated with number."), - args: [ - arg("table_number (number)", _t("The number of the character to look up from the current Unicode table in decimal format.")), - ], - returns: ["STRING"], - compute: function (tableNumber) { - const _tableNumber = Math.trunc(toNumber(tableNumber, this.locale)); - assert(() => _tableNumber >= 1, _t("The table_number (%s) is out of range.", _tableNumber.toString())); - return String.fromCharCode(_tableNumber); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // CLEAN - // ----------------------------------------------------------------------------- - const CLEAN = { - description: _t("Remove non-printable characters from a piece of text."), - args: [arg("text (string)", _t("The text whose non-printable characters are to be removed."))], - returns: ["STRING"], - compute: function (text) { - const _text = toString(text); - let cleanedStr = ""; - for (const char of _text) { - if (char && char.charCodeAt(0) > 31) { - cleanedStr += char; - } - } - return cleanedStr; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // CONCATENATE - // ----------------------------------------------------------------------------- - const CONCATENATE = { - description: _t("Appends strings to one another."), - args: [ - arg("string1 (string, range)", _t("The initial string.")), - arg("string2 (string, range, repeating)", _t("More strings to append in sequence.")), - ], - returns: ["STRING"], - compute: function (...values) { - return reduceAny(values, (acc, a) => acc + toString(a), ""); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // EXACT - // ----------------------------------------------------------------------------- - const EXACT = { - description: _t("Tests whether two strings are identical."), - args: [ - arg("string1 (string)", _t("The first string to compare.")), - arg("string2 (string)", _t("The second string to compare.")), - ], - returns: ["BOOLEAN"], - compute: function (string1, string2) { - return toString(string1) === toString(string2); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // FIND - // ----------------------------------------------------------------------------- - const FIND = { - description: _t("First position of string found in text, case-sensitive."), - args: [ - arg("search_for (string)", _t("The string to look for within text_to_search.")), - arg("text_to_search (string)", _t("The text to search for the first occurrence of search_for.")), - arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search.")), - ], - returns: ["NUMBER"], - compute: function (searchFor, textToSearch, startingAt = DEFAULT_STARTING_AT) { - const _searchFor = toString(searchFor); - const _textToSearch = toString(textToSearch); - const _startingAt = toNumber(startingAt, this.locale); - assert(() => _textToSearch !== "", _t(`The text_to_search must be non-empty.`)); - assert(() => _startingAt >= 1, _t("The starting_at (%s) must be greater than or equal to 1.", _startingAt.toString())); - const result = _textToSearch.indexOf(_searchFor, _startingAt - 1); - assert(() => result >= 0, _t("In [[FUNCTION_NAME]] evaluation, cannot find '%s' within '%s'.", _searchFor.toString(), _textToSearch)); - return result + 1; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // JOIN - // ----------------------------------------------------------------------------- - const JOIN = { - description: _t("Concatenates elements of arrays with delimiter."), - args: [ - arg("delimiter (string)", _t("The character or string to place between each concatenated value.")), - arg("value_or_array1 (string, range)", _t("The value or values to be appended using delimiter.")), - arg("value_or_array2 (string, range, repeating)", _t("More values to be appended using delimiter.")), - ], - returns: ["STRING"], - compute: function (delimiter, ...valuesOrArrays) { - const _delimiter = toString(delimiter); - return reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toString(a), ""); - }, - }; - // ----------------------------------------------------------------------------- - // LEFT - // ----------------------------------------------------------------------------- - const LEFT = { - description: _t("Substring from beginning of specified string."), - args: [ - arg("text (string)", _t("The string from which the left portion will be returned.")), - arg("number_of_characters (number, optional)", _t("The number of characters to return from the left side of string.")), - ], - returns: ["STRING"], - compute: function (text, ...args) { - const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1; - assert(() => _numberOfCharacters >= 0, _t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters.toString())); - return toString(text).substring(0, _numberOfCharacters); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // LEN - // ----------------------------------------------------------------------------- - const LEN = { - description: _t("Length of a string."), - args: [arg("text (string)", _t("The string whose length will be returned."))], - returns: ["NUMBER"], - compute: function (text) { - return toString(text).length; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // LOWER - // ----------------------------------------------------------------------------- - const LOWER = { - description: _t("Converts a specified string to lowercase."), - args: [arg("text (string)", _t("The string to convert to lowercase."))], - returns: ["STRING"], - compute: function (text) { - return toString(text).toLowerCase(); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // MID - // ----------------------------------------------------------------------------- - const MID = { - description: _t("A segment of a string."), - args: [ - arg("text (string)", _t("The string to extract a segment from.")), - arg(" (number)", _t("The index from the left of string from which to begin extracting. The first character in string has the index 1.")), - arg(" (number)", _t("The length of the segment to extract.")), - ], - returns: ["STRING"], - compute: function (text, starting_at, extract_length) { - const _text = toString(text); - const _starting_at = toNumber(starting_at, this.locale); - const _extract_length = toNumber(extract_length, this.locale); - assert(() => _starting_at >= 1, _t("The starting_at argument (%s) must be positive greater than one.", _starting_at.toString())); - assert(() => _extract_length >= 0, _t("The extract_length argument (%s) must be positive or null.", _extract_length.toString())); - return _text.slice(_starting_at - 1, _starting_at + _extract_length - 1); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // PROPER - // ----------------------------------------------------------------------------- - const PROPER = { - description: _t("Capitalizes each word in a specified string."), - 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.")), - ], - returns: ["STRING"], - compute: function (text) { - const _text = toString(text); - return _text.replace(wordRegex, (word) => { - return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); - }); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // REPLACE - // ----------------------------------------------------------------------------- - const REPLACE = { - description: _t("Replaces part of a text string with different text."), - args: [ - arg("text (string)", _t("The text, a part of which will be replaced.")), - arg("position (number)", _t("The position where the replacement will begin (starting from 1).")), - arg("length (number)", _t("The number of characters in the text to be replaced.")), - arg("new_text (string)", _t("The text which will be inserted into the original text.")), - ], - returns: ["STRING"], - compute: function (text, position, length, newText) { - const _position = toNumber(position, this.locale); - assert(() => _position >= 1, _t("The position (%s) must be greater than or equal to 1.", _position.toString())); - const _text = toString(text); - const _length = toNumber(length, this.locale); - const _newText = toString(newText); - return _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // RIGHT - // ----------------------------------------------------------------------------- - const RIGHT = { - description: _t("A substring from the end of a specified string."), - args: [ - arg("text (string)", _t("The string from which the right portion will be returned.")), - arg("number_of_characters (number, optional)", _t("The number of characters to return from the right side of string.")), - ], - returns: ["STRING"], - compute: function (text, ...args) { - const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1; - assert(() => _numberOfCharacters >= 0, _t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters.toString())); - const _text = toString(text); - const stringLength = _text.length; - return _text.substring(stringLength - _numberOfCharacters, stringLength); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SEARCH - // ----------------------------------------------------------------------------- - const SEARCH = { - description: _t("First position of string found in text, ignoring case."), - args: [ - arg("search_for (string)", _t("The string to look for within text_to_search.")), - arg("text_to_search (string)", _t("The text to search for the first occurrence of search_for.")), - arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search.")), - ], - returns: ["NUMBER"], - compute: function (searchFor, textToSearch, startingAt = DEFAULT_STARTING_AT) { - const _searchFor = toString(searchFor).toLowerCase(); - const _textToSearch = toString(textToSearch).toLowerCase(); - const _startingAt = toNumber(startingAt, this.locale); - assert(() => _textToSearch !== "", _t(`The text_to_search must be non-empty.`)); - assert(() => _startingAt >= 1, _t("The starting_at (%s) must be greater than or equal to 1.", _startingAt.toString())); - const result = _textToSearch.indexOf(_searchFor, _startingAt - 1); - assert(() => result >= 0, _t("In [[FUNCTION_NAME]] evaluation, cannot find '%s' within '%s'.", _searchFor, _textToSearch)); - return result + 1; - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SPLIT - // ----------------------------------------------------------------------------- - const SPLIT_DEFAULT_SPLIT_BY_EACH = true; - const SPLIT_DEFAULT_REMOVE_EMPTY_TEXT = true; - const SPLIT = { - description: _t("Split text by specific character delimiter(s)."), - args: [ - arg("text (string)", _t("The text to divide.")), - arg("delimiter (string)", _t("The character or characters to use to split text.")), - arg(`split_by_each (boolean, default=${SPLIT_DEFAULT_SPLIT_BY_EACH}})`, _t("Whether or not to divide text around each character contained in delimiter.")), - 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.")), - ], - returns: ["RANGE"], - compute: function (text, delimiter, splitByEach = SPLIT_DEFAULT_SPLIT_BY_EACH, removeEmptyText = SPLIT_DEFAULT_REMOVE_EMPTY_TEXT) { - const _text = toString(text); - const _delimiter = escapeRegExp(toString(delimiter)); - const _splitByEach = toBoolean(splitByEach); - const _removeEmptyText = toBoolean(removeEmptyText); - assert(() => _delimiter.length > 0, _t("The _delimiter (%s) must be not be empty.", _delimiter)); - const regex = _splitByEach ? new RegExp(`[${_delimiter}]`, "g") : new RegExp(_delimiter, "g"); - let result = _text.split(regex); - if (_removeEmptyText) { - result = result.filter((text) => text !== ""); - } - return transposeMatrix([result]); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // SUBSTITUTE - // ----------------------------------------------------------------------------- - const SUBSTITUTE = { - description: _t("Replaces existing text with new text in a string."), - args: [ - arg("text_to_search (string)", _t("The text within which to search and replace.")), - arg("search_for (string)", _t("The string to search for within text_to_search.")), - arg("replace_with (string)", _t("The string that will replace search_for.")), - arg("occurrence_number (number, optional)", _t("The instance of search_for within text_to_search to replace with replace_with. By default, all occurrences of search_for are replaced; however, if occurrence_number is specified, only the indicated instance of search_for is replaced.")), - ], - returns: ["NUMBER"], - compute: function (textToSearch, searchFor, replaceWith, occurrenceNumber) { - const _occurrenceNumber = toNumber(occurrenceNumber, this.locale); - assert(() => _occurrenceNumber >= 0, _t("The occurrenceNumber (%s) must be positive or null.", _occurrenceNumber.toString())); - const _textToSearch = toString(textToSearch); - const _searchFor = toString(searchFor); - if (_searchFor === "") { - return _textToSearch; - } - const _replaceWith = toString(replaceWith); - const reg = new RegExp(escapeRegExp(_searchFor), "g"); - if (_occurrenceNumber === 0) { - return _textToSearch.replace(reg, _replaceWith); - } - let n = 0; - return _textToSearch.replace(reg, (text) => (++n === _occurrenceNumber ? _replaceWith : text)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TEXTJOIN - // ----------------------------------------------------------------------------- - const TEXTJOIN = { - description: _t("Combines text from multiple strings and/or arrays."), - args: [ - arg("delimiter (string)", _t(" A string, possible empty, or a reference to a valid string. If empty, the text will be simply concatenated.")), - arg("ignore_empty (boolean)", _t("A boolean; if TRUE, empty cells selected in the text arguments won't be included in the result.")), - arg("text1 (string, range)", _t("Any text item. This could be a string, or an array of strings in a range.")), - arg("text2 (string, range, repeating)", _t("Additional text item(s).")), - ], - returns: ["STRING"], - compute: function (delimiter, ignoreEmpty, ...textsOrArrays) { - const _delimiter = toString(delimiter); - const _ignoreEmpty = toBoolean(ignoreEmpty); - let n = 0; - return reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toString(a) === "") ? (n++ ? acc + _delimiter : "") + toString(a) : acc, ""); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TRIM - // ----------------------------------------------------------------------------- - const TRIM = { - description: _t("Removes space characters."), - args: [ - arg("text (string)", _t("The text or reference to a cell containing text to be trimmed.")), - ], - returns: ["STRING"], - compute: function (text) { - return trimContent(toString(text)); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // UPPER - // ----------------------------------------------------------------------------- - const UPPER = { - description: _t("Converts a specified string to uppercase."), - args: [arg("text (string)", _t("The string to convert to uppercase."))], - returns: ["STRING"], - compute: function (text) { - return toString(text).toUpperCase(); - }, - isExported: true, - }; - // ----------------------------------------------------------------------------- - // TEXT - // ----------------------------------------------------------------------------- - const TEXT = { - description: _t("Converts a number to text according to a specified format."), - args: [ - arg("number (number)", _t("The number, date or time to format.")), - arg("format (string)", _t("The pattern by which to format the number, enclosed in quotation marks.")), - ], - returns: ["STRING"], - compute: function (number, format) { - const _number = toNumber(number, this.locale); - return formatValue(_number, { format: toString(format), locale: this.locale }); - }, - isExported: true, - }; - - var text = /*#__PURE__*/Object.freeze({ - __proto__: null, - CHAR: CHAR, - CLEAN: CLEAN, - CONCATENATE: CONCATENATE, - EXACT: EXACT, - FIND: FIND, - JOIN: JOIN, - LEFT: LEFT, - LEN: LEN, - LOWER: LOWER, - MID: MID, - PROPER: PROPER, - REPLACE: REPLACE, - RIGHT: RIGHT, - SEARCH: SEARCH, - SPLIT: SPLIT, - SUBSTITUTE: SUBSTITUTE, - TEXT: TEXT, - TEXTJOIN: TEXTJOIN, - TRIM: TRIM, - UPPER: UPPER - }); - - // ----------------------------------------------------------------------------- - // HYPERLINK - // ----------------------------------------------------------------------------- - const HYPERLINK = { - description: _t("Creates a hyperlink in a cell."), - 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.")), - ], - returns: ["STRING"], - compute: function (url, linkLabel) { - const processedUrl = toString(url).trim(); - const processedLabel = toString(linkLabel) || processedUrl; - if (processedUrl === "") - return processedLabel; - return markdownLink(processedLabel, processedUrl); - }, - isExported: true, - }; - - var web = /*#__PURE__*/Object.freeze({ - __proto__: null, - HYPERLINK: HYPERLINK - }); - - const categories = [ - { name: _t("Array"), functions: array }, - { name: _t("Database"), functions: database }, - { name: _t("Date"), functions: date }, - { name: _t("Filter"), functions: filter }, - { name: _t("Financial"), functions: financial }, - { name: _t("Info"), functions: info }, - { name: _t("Lookup"), functions: lookup }, - { name: _t("Logical"), functions: logical }, - { name: _t("Math"), functions: math }, - { name: _t("Misc"), functions: misc$1 }, - { name: _t("Operator"), functions: operators }, - { name: _t("Statistical"), functions: statistical }, - { name: _t("Text"), functions: text }, - { name: _t("Engineering"), functions: engineering }, - { name: _t("Web"), functions: web }, - ]; - const functionNameRegex = /^[A-Z0-9\_\.]+$/; - //------------------------------------------------------------------------------ - // Function registry - //------------------------------------------------------------------------------ - class FunctionRegistry extends Registry { - mapping = {}; - add(name, addDescr) { - name = name.toUpperCase(); - if (!functionNameRegex.test(name)) { - throw new Error(_t("Invalid function name %s. Function names can exclusively contain alphanumerical values separated by dots (.) or underscore (_)", name)); - } - const descr = addMetaInfoFromArg(addDescr); - validateArguments(descr.args); - this.mapping[name] = createComputeFunctionFromDescription(descr); - super.add(name, descr); - return this; - } - } - function createComputeFunctionFromDescription(descr) { - const computeValueAndFormat = "computeValueAndFormat" in descr; - const computeValue = "compute" in descr; - const computeFormat = "computeFormat" in descr; - if (!computeValueAndFormat && !computeValue) { - throw new Error("Invalid function description, need at least one 'compute' or 'computeValueAndFormat' function"); - } - if (computeValueAndFormat && (computeValue || computeFormat)) { - throw new Error("Invalid function description, cannot have both 'computeValueAndFormat' and 'compute'/'computeFormat' functions"); - } - if (computeValueAndFormat) { - return descr.computeValueAndFormat; - } - // case computeValue - return buildComputeFunctionFromDescription(descr); - } - function buildComputeFunctionFromDescription(descr) { - return function (...args) { - const computeValue = descr.compute.bind(this); - const computeFormat = descr.computeFormat?.bind(this) || (() => undefined); - const value = computeValue(...extractArgValuesFromArgs(args)); - const format = computeFormat(...args); - if (isMatrix(value)) { - if (format === undefined || isMatrix(format)) { - return value.map((col, i) => col.map((row, j) => ({ value: row, format: format?.[i]?.[j] }))); - } - } - else { - if (format === undefined || !isMatrix(format)) { - return { value, format }; - } - } - throw new Error("A format matrix should never be associated with a scalar value"); - }; - } - function extractArgValuesFromArgs(args) { - return args.map((arg) => { - if (arg === undefined) { - return undefined; - } - if (typeof arg === "function") { - return () => extractArgValueFromArg(arg()); - } - return extractArgValueFromArg(arg); - }); - } - function extractArgValueFromArg(arg) { - if (isMatrix(arg)) { - return matrixMap(arg, (data) => data.value); - } - return arg?.value; - } - const functionRegistry = new FunctionRegistry(); - for (let category of categories) { - const fns = category.functions; - for (let name in fns) { - const addDescr = fns[name]; - addDescr.category = addDescr.category || category.name; - name = name.replace(/_/g, "."); - functionRegistry.add(name, { isExported: false, ...addDescr }); - } - } - - class FunctionCodeBuilder { - scope; - code = ""; - constructor(scope = new Scope()) { - this.scope = scope; - } - append(...lines) { - this.code += lines.map((line) => line.toString()).join("\n") + "\n"; - } - return(expression) { - return new FunctionCodeImpl(this.scope, this.code, expression); - } - toString() { - return indentCode(this.code); - } - } - class FunctionCodeImpl { - scope; - returnExpression; - code; - constructor(scope, code, returnExpression) { - this.scope = scope; - this.returnExpression = returnExpression; - this.code = indentCode(code); - } - toString() { - return this.code; - } - wrapInClosure() { - const closureName = this.scope.nextVariableName(); - const code = new FunctionCodeBuilder(this.scope); - code.append(`const ${closureName} = () => {`); - code.append(this.code); - code.append(`return ${this.returnExpression};`); - code.append(`}`); - return code.return(closureName); - } - assignResultToVariable() { - if (this.scope.isAlreadyDeclared(this.returnExpression)) { - return this; - } - const variableName = this.scope.nextVariableName(); - const code = new FunctionCodeBuilder(this.scope); - code.append(this.code); - code.append(`const ${variableName} = ${this.returnExpression};`); - return code.return(variableName); - } - } - class Scope { - nextId = 1; - declaredVariables = new Set(); - nextVariableName() { - const name = `_${this.nextId++}`; - this.declaredVariables.add(name); - return name; - } - isAlreadyDeclared(name) { - return this.declaredVariables.has(name); - } - } - /** - * Takes a list of strings that might be single or multiline - * and maps them in a list of single line strings. - */ - function splitLines(str) { - return str - .split("\n") - .map((line) => line.trim()) - .filter((line) => line !== ""); - } - function indentCode(code) { - let result = ""; - let indentLevel = 0; - const lines = splitLines(code); - for (const line of lines) { - if (line.startsWith("}")) { - indentLevel--; - } - result += "\t".repeat(indentLevel) + line + "\n"; - if (line.endsWith("{")) { - indentLevel++; - } - } - return result.trim(); - } - /** * Tokenizer * @@ -15599,14 +7336,14 @@ } return null; } - const misc = { + const misc$1 = { "(": "LEFT_PAREN", ")": "RIGHT_PAREN", }; function tokenizeMisc(chars) { - if (chars.current() in misc) { + if (chars.current() in misc$1) { const value = chars.shift(); - const type = misc[value]; + const type = misc$1[value]; return { type, value }; } return null; @@ -15763,804 +7500,6 @@ } } - const functionRegex = /[a-zA-Z0-9\_]+(\.[a-zA-Z0-9\_]+)*/; - const UNARY_OPERATORS_PREFIX = ["-", "+"]; - const UNARY_OPERATORS_POSTFIX = ["%"]; - const ASSOCIATIVE_OPERATORS = ["*", "+", "&"]; - const OP_PRIORITY = { - "^": 30, - "%": 30, - "*": 20, - "/": 20, - "+": 15, - "-": 15, - "&": 13, - ">": 10, - "<>": 10, - ">=": 10, - "<": 10, - "<=": 10, - "=": 10, - }; - /** - * Parse the next operand in an arithmetic expression. - * e.g. - * for 1+2*3, the next operand is 1 - * for (1+2)*3, the next operand is (1+2) - * for SUM(1,2)+3, the next operand is SUM(1,2) - */ - function parseOperand(tokens) { - const current = tokens.shift(); - if (!current) { - throw new BadExpressionError(DEFAULT_ERROR_MESSAGE); - } - switch (current.type) { - case "DEBUGGER": - const next = parseExpression(tokens, 1000); - next.debug = true; - return next; - case "NUMBER": - return { type: "NUMBER", value: parseNumber(current.value, DEFAULT_LOCALE) }; - case "STRING": - return { type: "STRING", value: removeStringQuotes(current.value) }; - case "INVALID_REFERENCE": - throw new InvalidReferenceError(); - case "REFERENCE": - if (tokens[0]?.value === ":" && tokens[1]?.type === "REFERENCE") { - tokens.shift(); - const rightReference = tokens.shift(); - return { - type: "REFERENCE", - value: `${current.value}:${rightReference?.value}`, - }; - } - return { - type: "REFERENCE", - value: current.value, - }; - case "SYMBOL": - const value = current.value; - const nextToken = tokens[0]; - if (nextToken?.type === "LEFT_PAREN" && functionRegex.test(current.value)) { - const args = parseFunctionArgs(tokens); - return { type: "FUNCALL", value: value, args }; - } - const upperCaseValue = value.toUpperCase(); - if (upperCaseValue === "TRUE" || upperCaseValue === "FALSE") { - return { type: "BOOLEAN", value: upperCaseValue === "TRUE" }; - } - throw new BadExpressionError(_t("Invalid formula")); - case "LEFT_PAREN": - const result = parseExpression(tokens); - consumeOrThrow(tokens, "RIGHT_PAREN", _t("Missing closing parenthesis")); - return result; - case "OPERATOR": - const operator = current.value; - if (UNARY_OPERATORS_PREFIX.includes(operator)) { - return { - type: "UNARY_OPERATION", - value: operator, - operand: parseExpression(tokens, OP_PRIORITY[operator]), - }; - } - throw new BadExpressionError(_t("Unexpected token: %s", current.value)); - default: - throw new BadExpressionError(_t("Unexpected token: %s", current.value)); - } - } - function parseFunctionArgs(tokens) { - consumeOrThrow(tokens, "LEFT_PAREN", _t("Missing opening parenthesis")); - const nextToken = tokens[0]; - if (nextToken?.type === "RIGHT_PAREN") { - consumeOrThrow(tokens, "RIGHT_PAREN"); - return []; - } - const args = []; - args.push(parseOneFunctionArg(tokens)); - while (tokens[0]?.type !== "RIGHT_PAREN") { - consumeOrThrow(tokens, "ARG_SEPARATOR", _t("Wrong function call")); - args.push(parseOneFunctionArg(tokens)); - } - consumeOrThrow(tokens, "RIGHT_PAREN"); - return args; - } - function parseOneFunctionArg(tokens) { - const nextToken = tokens[0]; - if (nextToken?.type === "ARG_SEPARATOR" || nextToken?.type === "RIGHT_PAREN") { - // arg is empty: "sum(1,,2)" "sum(,1)" "sum(1,)" - return { type: "EMPTY", value: "" }; - } - return parseExpression(tokens); - } - function consumeOrThrow(tokens, type, message = DEFAULT_ERROR_MESSAGE) { - const token = tokens.shift(); - if (!token || token.type !== type) { - throw new BadExpressionError(message); - } - } - function parseExpression(tokens, parent_priority = 0) { - if (tokens.length === 0) { - throw new BadExpressionError(DEFAULT_ERROR_MESSAGE); - } - let left = parseOperand(tokens); - // as long as we have operators with higher priority than the parent one, - // continue parsing the expression because it is a child sub-expression - while (tokens[0]?.type === "OPERATOR" && OP_PRIORITY[tokens[0].value] > parent_priority) { - const operator = tokens.shift().value; - if (UNARY_OPERATORS_POSTFIX.includes(operator)) { - left = { - type: "UNARY_OPERATION", - value: operator, - operand: left, - postfix: true, - }; - } - else { - const right = parseExpression(tokens, OP_PRIORITY[operator]); - left = { - type: "BIN_OPERATION", - value: operator, - left, - right, - }; - } - } - return left; - } - /** - * Parse an expression (as a string) into an AST. - */ - function parse(str) { - return parseTokens(tokenize(str)); - } - function parseTokens(tokens) { - tokens = tokens.filter((x) => x.type !== "SPACE"); - if (tokens[0].value === "=") { - tokens.splice(0, 1); - } - const result = parseExpression(tokens); - if (tokens.length) { - throw new BadExpressionError(DEFAULT_ERROR_MESSAGE); - } - return result; - } - /** - * Allows to visit all nodes of an AST and apply a mapping function - * to nodes of a specific type. - * Useful if you want to convert some part of a formula. - * - * @example - * convertAstNodes(ast, "FUNCALL", convertFormulaToExcel) - * - * function convertFormulaToExcel(ast: ASTFuncall) { - * // ... - * return modifiedAst - * } - */ - function convertAstNodes(ast, type, fn) { - return mapAst(ast, (ast) => { - if (ast.type === type) { - return fn(ast); - } - return ast; - }); - } - function iterateAstNodes(ast) { - return Array.from(astIterator(ast)); - } - function* astIterator(ast) { - yield ast; - switch (ast.type) { - case "FUNCALL": - for (const arg of ast.args) { - yield* astIterator(arg); - } - break; - case "UNARY_OPERATION": - yield* astIterator(ast.operand); - break; - case "BIN_OPERATION": - yield* astIterator(ast.left); - yield* astIterator(ast.right); - break; - } - } - function mapAst(ast, fn) { - ast = fn(ast); - switch (ast.type) { - case "FUNCALL": - return { - ...ast, - args: ast.args.map((child) => mapAst(child, fn)), - }; - case "UNARY_OPERATION": - return { - ...ast, - operand: mapAst(ast.operand, fn), - }; - case "BIN_OPERATION": - return { - ...ast, - right: mapAst(ast.right, fn), - left: mapAst(ast.left, fn), - }; - default: - return ast; - } - } - /** - * Converts an ast formula to the corresponding string - */ - function astToFormula(ast) { - switch (ast.type) { - case "FUNCALL": - const args = ast.args.map((arg) => astToFormula(arg)); - return `${ast.value}(${args.join(",")})`; - case "NUMBER": - return ast.value.toString(); - case "REFERENCE": - return ast.value; - case "STRING": - return `"${ast.value}"`; - case "BOOLEAN": - return ast.value ? "TRUE" : "FALSE"; - case "UNARY_OPERATION": - return ast.postfix - ? leftOperandToFormula(ast) + ast.value - : ast.value + rightOperandToFormula(ast); - case "BIN_OPERATION": - return leftOperandToFormula(ast) + ast.value + rightOperandToFormula(ast); - default: - return ast.value; - } - } - /** - * Convert the left operand of a binary operation to the corresponding string - * and enclose the result inside parenthesis if necessary. - */ - function leftOperandToFormula(operationAST) { - const mainOperator = operationAST.value; - const leftOperation = "left" in operationAST ? operationAST.left : operationAST.operand; - const leftOperator = leftOperation.value; - const needParenthesis = leftOperation.type === "BIN_OPERATION" && OP_PRIORITY[leftOperator] < OP_PRIORITY[mainOperator]; - return needParenthesis ? `(${astToFormula(leftOperation)})` : astToFormula(leftOperation); - } - /** - * Convert the right operand of a binary or unary operation to the corresponding string - * and enclose the result inside parenthesis if necessary. - */ - function rightOperandToFormula(operationAST) { - const mainOperator = operationAST.value; - const rightOperation = "right" in operationAST ? operationAST.right : operationAST.operand; - const rightPriority = OP_PRIORITY[rightOperation.value]; - const mainPriority = OP_PRIORITY[mainOperator]; - let needParenthesis = false; - if (rightOperation.type !== "BIN_OPERATION") { - needParenthesis = false; - } - else if (rightPriority < mainPriority) { - needParenthesis = true; - } - else if (rightPriority === mainPriority && !ASSOCIATIVE_OPERATORS.includes(mainOperator)) { - needParenthesis = true; - } - return needParenthesis ? `(${astToFormula(rightOperation)})` : astToFormula(rightOperation); - } - - var State; - (function (State) { - /** - * Initial state. - * Expecting any reference for the left part of a range - * e.g. "A1", "1", "A", "Sheet1!A1", "Sheet1!A" - */ - State[State["LeftRef"] = 0] = "LeftRef"; - /** - * Expecting any reference for the right part of a range - * e.g. "A1", "1", "A", "Sheet1!A1", "Sheet1!A" - */ - State[State["RightRef"] = 1] = "RightRef"; - /** - * Expecting the separator without any constraint on the right part - */ - State[State["Separator"] = 2] = "Separator"; - /** - * Expecting the separator for a full column range - */ - State[State["FullColumnSeparator"] = 3] = "FullColumnSeparator"; - /** - * Expecting the separator for a full row range - */ - State[State["FullRowSeparator"] = 4] = "FullRowSeparator"; - /** - * Expecting the right part of a full column range - * e.g. "1", "A1" - */ - State[State["RightColumnRef"] = 5] = "RightColumnRef"; - /** - * Expecting the right part of a full row range - * e.g. "A", "A1" - */ - State[State["RightRowRef"] = 6] = "RightRowRef"; - /** - * Final state. A range has been matched - */ - State[State["Found"] = 7] = "Found"; - })(State || (State = {})); - const goTo = (state, guard = () => true) => [ - { - goTo: state, - guard, - }, - ]; - const goToMulti = (state, guard = () => true) => ({ - goTo: state, - guard, - }); - const machine = { - [State.LeftRef]: { - REFERENCE: goTo(State.Separator), - NUMBER: goTo(State.FullRowSeparator), - SYMBOL: [ - goToMulti(State.FullColumnSeparator, (token) => isColReference(token.value)), - goToMulti(State.FullRowSeparator, (token) => isRowReference(token.value)), - ], - }, - [State.FullColumnSeparator]: { - SPACE: goTo(State.FullColumnSeparator), - OPERATOR: goTo(State.RightColumnRef, (token) => token.value === ":"), - }, - [State.FullRowSeparator]: { - SPACE: goTo(State.FullRowSeparator), - OPERATOR: goTo(State.RightRowRef, (token) => token.value === ":"), - }, - [State.Separator]: { - SPACE: goTo(State.Separator), - OPERATOR: goTo(State.RightRef, (token) => token.value === ":"), - }, - [State.RightRef]: { - SPACE: goTo(State.RightRef), - NUMBER: goTo(State.Found), - REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)), - SYMBOL: goTo(State.Found, (token) => isColHeader(token.value)), - }, - [State.RightColumnRef]: { - SPACE: goTo(State.RightColumnRef), - SYMBOL: goTo(State.Found, (token) => isColHeader(token.value)), - REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)), - }, - [State.RightRowRef]: { - SPACE: goTo(State.RightRowRef), - NUMBER: goTo(State.Found), - REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)), - }, - [State.Found]: {}, - }; - /** - * Check if the list of tokens starts with a sequence of tokens representing - * a range. - * If a range is found, the sequence is removed from the list and is returned - * as a single token. - */ - function matchReference(tokens) { - let head = 0; - let transitions = machine[State.LeftRef]; - const matchedTokens = []; - while (transitions !== undefined) { - const token = tokens[head++]; - if (!token) { - return null; - } - const transition = transitions[token.type]?.find((transition) => transition.guard(token)); - const nextState = transition ? transition.goTo : undefined; - switch (nextState) { - case undefined: - return null; - case State.Found: - matchedTokens.push(token); - tokens.splice(0, head); - return { - type: "REFERENCE", - value: concat(matchedTokens.map((token) => token.value)), - }; - default: - transitions = machine[nextState]; - matchedTokens.push(token); - break; - } - } - return null; - } - /** - * Take the result of the tokenizer and transform it to be usable in the - * manipulations of range - * - * @param formula - */ - function rangeTokenize(formula, locale = DEFAULT_LOCALE) { - const tokens = tokenize(formula, locale); - const result = []; - while (tokens.length) { - result.push(matchReference(tokens) || tokens.shift()); - } - return result; - } - - const functions$2 = functionRegistry.content; - const OPERATOR_MAP = { - "=": "EQ", - "+": "ADD", - "-": "MINUS", - "*": "MULTIPLY", - "/": "DIVIDE", - ">=": "GTE", - "<>": "NE", - ">": "GT", - "<=": "LTE", - "<": "LT", - "^": "POWER", - "&": "CONCATENATE", - }; - const UNARY_OPERATOR_MAP = { - "-": "UMINUS", - "+": "UPLUS", - "%": "UNARY.PERCENT", - }; - // this cache contains all compiled function code, grouped by "structure". For - // example, "=2*sum(A1:A4)" and "=2*sum(B1:B4)" are compiled into the same - // structural function. - // It is only exported for testing purposes - const functionCache = {}; - // ----------------------------------------------------------------------------- - // COMPILER - // ----------------------------------------------------------------------------- - function compile(formula) { - const tokens = rangeTokenize(formula); - const { dependencies, constantValues } = formulaArguments(tokens); - const cacheKey = compilationCacheKey(tokens, dependencies, constantValues); - if (!functionCache[cacheKey]) { - const ast = parseTokens([...tokens]); - const scope = new Scope(); - if (ast.type === "BIN_OPERATION" && ast.value === ":") { - throw new BadExpressionError(_t("Invalid formula")); - } - if (ast.type === "EMPTY") { - throw new BadExpressionError(_t("Invalid formula")); - } - const compiledAST = compileAST(ast); - const code = new FunctionCodeBuilder(); - code.append(`// ${cacheKey}`); - code.append(compiledAST); - code.append(`return ${compiledAST.returnExpression};`); - let baseFunction = new Function("deps", // the dependencies in the current formula - "ref", // a function to access a certain dependency at a given index - "range", // same as above, but guarantee that the result is in the form of a range - "ctx", code.toString()); - functionCache[cacheKey] = { - // @ts-ignore - execute: baseFunction, - }; - /** - * This function compile the function arguments. It is mostly straightforward, - * except that there is a non trivial transformation in one situation: - * - * If a function argument is asking for a range, and get a cell, we transform - * the cell value into a range. This allow the grid model to differentiate - * between a cell value and a non cell value. - */ - function compileFunctionArgs(ast) { - const { args } = ast; - const functionName = ast.value.toUpperCase(); - const functionDefinition = functions$2[functionName]; - if (!functionDefinition) { - throw new UnknownFunctionError(ast.value); - } - assertEnoughArgs(ast); - const compiledArgs = []; - for (let i = 0; i < args.length; i++) { - const argToFocus = functionDefinition.getArgToFocus(i + 1) - 1; - const argDefinition = functionDefinition.args[argToFocus]; - const currentArg = args[i]; - const argTypes = argDefinition.type || []; - // detect when an argument need to be evaluated as a meta argument - const isMeta = argTypes.includes("META"); - // detect when an argument need to be evaluated as a lazy argument - const isLazy = argDefinition.lazy; - const hasRange = argTypes.some((t) => isRangeType(t)); - const isRangeOnly = argTypes.every((t) => isRangeType(t)); - if (isRangeOnly) { - if (!isRangeInput(currentArg)) { - throw new BadExpressionError(_t("Function %s expects the parameter %s to be reference to a cell or range, not a %s.", functionName, (i + 1).toString(), currentArg.type.toLowerCase())); - } - } - const compiledAST = compileAST(currentArg, isMeta, hasRange, { - functionName, - paramIndex: i + 1, - }); - compiledArgs.push(isLazy ? compiledAST.wrapInClosure() : compiledAST); - } - return compiledArgs; - } - /** - * This function compiles all the information extracted by the parser into an - * executable code for the evaluation of the cells content. It uses a cash to - * not reevaluate identical code structures. - * - * The function is sensitive to parameter “isMeta”. This - * parameter may vary when compiling function arguments: - * isMeta: In some cases the function arguments expects information on the - * cell/range other than the associated value(s). For example the COLUMN - * function needs to receive as argument the coordinates of a cell rather - * than its value. For this we have meta arguments. - */ - function compileAST(ast, isMeta = false, hasRange = false, referenceVerification = {}) { - const code = new FunctionCodeBuilder(scope); - if (ast.type !== "REFERENCE" && !(ast.type === "BIN_OPERATION" && ast.value === ":")) { - if (isMeta) { - throw new BadExpressionError(_t(`Argument must be a reference to a cell or range.`)); - } - } - if (ast.debug) { - code.append("debugger;"); - } - switch (ast.type) { - case "BOOLEAN": - return code.return(`{ value: ${ast.value} }`); - case "NUMBER": - return code.return(`{ value: this.constantValues.numbers[${constantValues.numbers.indexOf(ast.value)}] }`); - case "STRING": - return code.return(`{ value: this.constantValues.strings[${constantValues.strings.indexOf(ast.value)}] }`); - case "REFERENCE": - const referenceIndex = dependencies.indexOf(ast.value); - if (hasRange) { - return code.return(`range(deps[${referenceIndex}])`); - } - else { - return code.return(`ref(deps[${referenceIndex}], ${isMeta ? "true" : "false"}, "${referenceVerification.functionName || OPERATOR_MAP["="]}", ${referenceVerification.paramIndex})`); - } - case "FUNCALL": - const args = compileFunctionArgs(ast).map((arg) => arg.assignResultToVariable()); - code.append(...args); - const fnName = ast.value.toUpperCase(); - code.append(`ctx.__lastFnCalled = '${fnName}';`); - return code.return(`ctx['${fnName}'](${args.map((arg) => arg.returnExpression)})`); - case "UNARY_OPERATION": { - const fnName = UNARY_OPERATOR_MAP[ast.value]; - const operand = compileAST(ast.operand, false, false, { - functionName: fnName, - }).assignResultToVariable(); - code.append(operand); - code.append(`ctx.__lastFnCalled = '${fnName}';`); - return code.return(`ctx['${fnName}'](${operand.returnExpression})`); - } - case "BIN_OPERATION": { - const fnName = OPERATOR_MAP[ast.value]; - const left = compileAST(ast.left, false, false, { - functionName: fnName, - }).assignResultToVariable(); - const right = compileAST(ast.right, false, false, { - functionName: fnName, - }).assignResultToVariable(); - code.append(left); - code.append(right); - code.append(`ctx.__lastFnCalled = '${fnName}';`); - return code.return(`ctx['${fnName}'](${left.returnExpression}, ${right.returnExpression})`); - } - case "EMPTY": - return code.return("undefined"); - } - } - } - const compiledFormula = { - execute: functionCache[cacheKey].execute, - dependencies, - constantValues, - tokens, - }; - return compiledFormula; - } - /** - * Compute a cache key for the formula. - * References, numbers and strings are replaced with placeholders because - * the compiled formula does not depend on their actual value. - * Both `=A1+1+"2"` and `=A2+2+"3"` are compiled to the exact same function. - * - * Spaces are also ignored to compute the cache key. - * - * A formula `=A1+A2+SUM(2, 2, "2")` have the cache key `=|0|+|1|+SUM(|N0|,|N0|,|S0|)` - */ - function compilationCacheKey(tokens, dependencies, constantValues) { - return concat(tokens.map((token) => { - switch (token.type) { - case "STRING": - const value = removeStringQuotes(token.value); - return `|S${constantValues.strings.indexOf(value)}|`; - case "NUMBER": - return `|N${constantValues.numbers.indexOf(parseNumber(token.value, DEFAULT_LOCALE))}|`; - case "REFERENCE": - case "INVALID_REFERENCE": - return `|${dependencies.indexOf(token.value)}|`; - case "SPACE": - return ""; - default: - return token.value; - } - })); - } - /** - * Return formula arguments which are references, strings and numbers. - */ - function formulaArguments(tokens) { - const constantValues = { - numbers: [], - strings: [], - }; - const dependencies = []; - for (const token of tokens) { - switch (token.type) { - case "INVALID_REFERENCE": - case "REFERENCE": - dependencies.push(token.value); - break; - case "STRING": - const value = removeStringQuotes(token.value); - if (!constantValues.strings.includes(value)) { - constantValues.strings.push(value); - } - break; - case "NUMBER": { - const value = parseNumber(token.value, DEFAULT_LOCALE); - if (!constantValues.numbers.includes(value)) { - constantValues.numbers.push(value); - } - break; - } - } - } - return { - dependencies, - constantValues, - }; - } - /** - * Check if arguments are supplied in the correct quantities - */ - function assertEnoughArgs(ast) { - const nbrArg = ast.args.length; - const functionName = ast.value.toUpperCase(); - const functionDefinition = functions$2[functionName]; - if (nbrArg < functionDefinition.minArgRequired) { - throw new BadExpressionError(_t("Invalid number of arguments for the %s function. Expected %s minimum, but got %s instead.", functionName, functionDefinition.minArgRequired.toString(), nbrArg.toString())); - } - if (nbrArg > functionDefinition.maxArgPossible) { - throw new BadExpressionError(_t("Invalid number of arguments for the %s function. Expected %s maximum, but got %s instead.", functionName, functionDefinition.maxArgPossible.toString(), nbrArg.toString())); - } - const repeatableArgs = functionDefinition.nbrArgRepeating; - if (repeatableArgs > 1) { - const unrepeatableArgs = functionDefinition.args.length - repeatableArgs; - const repeatingArgs = nbrArg - unrepeatableArgs; - if (repeatingArgs % repeatableArgs !== 0) { - throw new BadExpressionError(_t("Invalid number of arguments for the %s function. Expected all arguments after position %s to be supplied by groups of %s arguments", functionName, unrepeatableArgs.toString(), repeatableArgs.toString())); - } - } - } - function isRangeType(type) { - return type.startsWith("RANGE"); - } - function isRangeInput(arg) { - if (arg.type === "REFERENCE") { - return true; - } - if (arg.type === "FUNCALL") { - const fnDef = functions$2[arg.value.toUpperCase()]; - return fnDef && isRangeType(fnDef.returns[0]); - } - return false; - } - - /** - * Add the following information on tokens: - * - length - * - start - * - end - */ - function enrichTokens(tokens) { - let current = 0; - return tokens.map((x) => { - const len = x.value.toString().length; - const token = Object.assign({}, x, { - start: current, - end: current + len, - length: len, - }); - current = token.end; - return token; - }); - } - /** - * add on each token the length, start and end - * also matches the opening to its closing parenthesis (using the same number) - */ - function mapParenthesis(tokens) { - let maxParen = 1; - const stack = []; - return tokens.map((token) => { - if (token.type === "LEFT_PAREN") { - stack.push(maxParen); - token.parenIndex = maxParen; - maxParen++; - } - else if (token.type === "RIGHT_PAREN") { - token.parenIndex = stack.pop(); - } - return token; - }); - } - /** - * add on each token its parent function and the index corresponding to - * its position as an argument of the function. - * In this example "=MIN(42,SUM(MAX(1,2),3))": - * - the parent function of the token correspond to number 42 is the MIN function - * - the argument position of the token correspond to number 42 is 0 - * - the parent function of the token correspond to number 3 is the SUM function - * - the argument position of the token correspond to number 3 is 1 - */ - function mapParentFunction(tokens) { - let stack = []; - let functionStarted = ""; - const res = tokens.map((token, i) => { - if (!["SPACE", "LEFT_PAREN"].includes(token.type)) { - functionStarted = ""; - } - switch (token.type) { - case "SYMBOL": - functionStarted = token.value; - break; - case "LEFT_PAREN": - stack.push({ parent: functionStarted, argPosition: 0 }); - functionStarted = ""; - break; - case "RIGHT_PAREN": - stack.pop(); - break; - case "ARG_SEPARATOR": - if (stack.length) { - // increment position on current function - stack[stack.length - 1].argPosition++; - } - break; - } - if (stack.length) { - const functionContext = stack[stack.length - 1]; - if (functionContext.parent) { - token.functionContext = Object.assign({}, functionContext); - } - } - return token; - }); - return res; - } - /** - * Take the result of the tokenizer and transform it to be usable in the composer. - * - * @param formula - */ - function composerTokenize(formula, locale) { - const tokens = rangeTokenize(formula, locale); - return mapParentFunction(mapParenthesis(enrichTokens(tokens))); - } - - const functions$1 = functionRegistry.content; - function isExportableToExcel(tokens) { - try { - const nonExportableFunctions = iterateAstNodes(parseTokens(tokens)).filter((ast) => ast.type === "FUNCALL" && !functions$1[ast.value.toUpperCase()]?.isExported); - return nonExportableFunctions.length === 0; - } - catch (error) { - return false; - } - } - function isValidLocale(locale) { if (!(locale && typeof locale === "object" && @@ -16786,6 +7725,9 @@ const newValue = threshold.type === "formula" ? modified.slice(1) : modified; return { ...threshold, value: newValue }; } + function getDateTimeFormat(locale) { + return locale.dateFormat + " " + locale.timeFormat; + } const linkSheet = { name: _t("Link sheet"), @@ -19935,6 +10877,8495 @@ undo: undo }); + //------------------------------------------------------------------------------ + // Arg description DSL + //------------------------------------------------------------------------------ + const ARG_REGEXP = /(.*?)\((.*?)\)(.*)/; + const ARG_TYPES = [ + "ANY", + "BOOLEAN", + "DATE", + "NUMBER", + "STRING", + "RANGE", + "RANGE", + "RANGE", + "RANGE", + "RANGE", + "META", + ]; + function arg(definition, description = "") { + return makeArg(definition, description); + } + function makeArg(str, description) { + let parts = str.match(ARG_REGEXP); + let name = parts[1].trim(); + let types = []; + let isOptional = false; + let isRepeating = false; + let isLazy = false; + let defaultValue; + for (let param of parts[2].split(",")) { + const key = param.trim().toUpperCase(); + let type = ARG_TYPES.find((t) => key === t); + if (type) { + types.push(type); + } + else if (key === "RANGE") { + types.push("RANGE"); + } + else if (key === "OPTIONAL") { + isOptional = true; + } + else if (key === "REPEATING") { + isRepeating = true; + } + else if (key === "LAZY") { + isLazy = true; + } + else if (key.startsWith("DEFAULT=")) { + defaultValue = param.trim().slice(8); + } + } + const result = { + name, + description, + type: types, + }; + if (isOptional) { + result.optional = true; + } + if (isRepeating) { + result.repeating = true; + } + if (isLazy) { + result.lazy = true; + } + if (defaultValue !== undefined) { + result.default = true; + result.defaultValue = defaultValue; + } + return result; + } + /** + * This function adds on description more general information derived from the + * arguments. + * + * This information is useful during compilation. + */ + function addMetaInfoFromArg(addDescr) { + let countArg = 0; + let minArg = 0; + let repeatingArg = 0; + for (let arg of addDescr.args) { + countArg++; + if (!arg.optional && !arg.repeating && !arg.default) { + minArg++; + } + if (arg.repeating) { + repeatingArg++; + } + } + const descr = addDescr; + descr.minArgRequired = minArg; + descr.maxArgPossible = repeatingArg ? Infinity : countArg; + descr.nbrArgRepeating = repeatingArg; + descr.getArgToFocus = argTargeting(countArg, repeatingArg); + descr.hidden = addDescr.hidden || false; + return descr; + } + /** + * Returns a function allowing finding which argument corresponds a position + * in a function. This is particularly useful for functions with repeatable + * arguments. + * + * Indeed the function makes it possible to etablish corespondance between + * arguments when the number of arguments supplied is greater than the number of + * arguments defined by the function. + * + * Ex: + * + * in the formula "=SUM(11, 55, 66)" which is defined like this "SUM(value1, [value2, ...])" + * - 11 corresponds to the value1 argument => position will be 1 + * - 55 corresponds to the [value2, ...] argument => position will be 2 + * - 66 corresponds to the [value2, ...] argument => position will be 2 + * + * in the formula "=AVERAGE.WEIGHTED(1, 2, 3, 4, 5, 6)" which is defined like this + * "AVERAGE.WEIGHTED(values, weights, [additional_values, ...], [additional_weights, ...])" + * - 1 corresponds to the values argument => position will be 1 + * - 2 corresponds to the weights argument => position will be 2 + * - 3 corresponds to the [additional_values, ...] argument => position will be 3 + * - 4 corresponds to the [additional_weights, ...] argument => position will be 4 + * - 5 corresponds to the [additional_values, ...] argument => position will be 3 + * - 6 corresponds to the [additional_weights, ...] argument => position will be 4 + */ + function argTargeting(countArg, repeatingArg) { + if (!repeatingArg) { + return (argPosition) => argPosition; + } + if (repeatingArg === 1) { + return (argPosition) => Math.min(argPosition, countArg); + } + const argBeforeRepeat = countArg - repeatingArg; + return (argPosition) => { + if (argPosition <= argBeforeRepeat) { + return argPosition; + } + const argAfterRepeat = (argPosition - argBeforeRepeat) % repeatingArg || repeatingArg; + return argBeforeRepeat + argAfterRepeat; + }; + } + //------------------------------------------------------------------------------ + // Argument validation + //------------------------------------------------------------------------------ + function validateArguments(args) { + let previousArgRepeating = false; + let previousArgOptional = false; + let previousArgDefault = false; + for (let current of args) { + if (current.type.includes("META") && current.type.length > 1) { + throw new Error(_t("Function ${name} has an argument that has been declared with more than one type whose type 'META'. The 'META' type can only be declared alone.")); + } + if (previousArgRepeating && !current.repeating) { + throw new Error(_t("Function ${name} has no-repeatable arguments declared after repeatable ones. All repeatable arguments must be declared last.")); + } + const previousIsOptional = previousArgOptional || previousArgRepeating || previousArgDefault; + const currentIsntOptional = !(current.optional || current.repeating || current.default); + if (previousIsOptional && currentIsntOptional) { + throw new Error(_t("Function ${name} has at mandatory arguments declared after optional ones. All optional arguments must be after all mandatory arguments.")); + } + previousArgRepeating = current.repeating; + previousArgOptional = current.optional; + previousArgDefault = current.default; + } + } + + function assertSingleColOrRow(errorStr, arg) { + assert(() => arg.length === 1 || arg[0].length === 1, errorStr); + } + function assertSameDimensions(errorStr, ...args) { + if (args.every(isMatrix)) { + const cols = args[0].length; + const rows = args[0][0].length; + for (const arg of args) { + assert(() => arg.length === cols && arg[0].length === rows, errorStr); + } + return; + } + if (args.some((arg) => Array.isArray(arg) && (arg.length !== 1 || arg[0].length !== 1))) { + throw new Error(errorStr); + } + } + function assertPositive(errorStr, arg) { + assert(() => arg > 0, errorStr); + } + function assertSquareMatrix(errorStr, arg) { + assert(() => arg.length === arg[0].length, errorStr); + } + function isNumberMatrix(arg) { + return arg.every((row) => row.every((val) => typeof val === "number")); + } + + function getUnitMatrix(n) { + const matrix = Array(n); + for (let i = 0; i < n; i++) { + matrix[i] = Array(n).fill(0); + matrix[i][i] = 1; + } + return matrix; + } + /** + * Invert a matrix and compute its determinant using Gaussian Elimination. + * + * The Matrix should be a square matrix, and should be indexed [col][row] instead of the + * standard mathematical indexing [row][col]. + */ + function invertMatrix(M) { + // Use Gaussian Elimination to calculate the inverse: + // (1) 'augment' the matrix (left) by the identity (on the right) + // (2) Turn the matrix on the left into the identity using elementary row operations + // (3) The matrix on the right becomes the inverse (was the identity matrix) + // + // There are 3 elementary row operations: + // (a) Swap 2 rows. This multiply the determinant by -1. + // (b) Multiply a row by a scalar. This multiply the determinant by that scalar. + // (c) Add to a row a multiple of another row. This does not change the determinant. + if (M.length !== M[0].length) { + throw new Error(`Function [[FUNCTION_NAME]] invert matrix error, only square matrices are invertible`); + } + let determinant = 1; + const dim = M.length; + const I = getUnitMatrix(dim); + const C = M.map((row) => row.slice()); + // Perform elementary row operations + for (let pivot = 0; pivot < dim; pivot++) { + let diagonalElement = C[pivot][pivot]; + // if we have a 0 on the diagonal we'll need to swap with a lower row + if (diagonalElement === 0) { + //look through every row below the i'th row + for (let row = pivot + 1; row < dim; row++) { + //if the ii'th row has a non-0 in the i'th col, swap it with that row + if (C[pivot][row] != 0) { + swapMatrixRows(C, pivot, row); + swapMatrixRows(I, pivot, row); + determinant *= -1; + break; + } + } + diagonalElement = C[pivot][pivot]; + //if it's still 0, matrix isn't invertible + if (diagonalElement === 0) { + return { determinant: 0 }; + } + } + // Scale this row down by e (so we have a 1 on the diagonal) + for (let col = 0; col < dim; col++) { + C[col][pivot] = C[col][pivot] / diagonalElement; + I[col][pivot] = I[col][pivot] / diagonalElement; + } + determinant *= diagonalElement; + // Subtract a multiple of the current row from ALL of + // the other rows so that there will be 0's in this column in the + // rows above and below this one + for (let row = 0; row < dim; row++) { + if (row === pivot) { + continue; + } + // We want to change this element to 0 + const e = C[pivot][row]; + // Subtract (the row above(or below) scaled by e) from (the + // current row) but start at the i'th column and assume all the + // stuff left of diagonal is 0 (which it should be if we made this + // algorithm correctly) + for (let col = 0; col < dim; col++) { + C[col][row] -= e * C[col][pivot]; + I[col][row] -= e * I[col][pivot]; + } + } + } + // We've done all operations, C should be the identity matrix I should be the inverse + return { inverted: I, determinant }; + } + function swapMatrixRows(matrix, row1, row2) { + for (let i = 0; i < matrix.length; i++) { + const tmp = matrix[i][row1]; + matrix[i][row1] = matrix[i][row2]; + matrix[i][row2] = tmp; + } + } + /** + * Matrix multiplication of 2 matrices. + * ex: matrix1 : n x l, matrix2 : m x n => result : m x l + * + * Note: we use indexing [col][row] instead of the standard mathematical notation [row][col] + */ + function multiplyMatrices(matrix1, matrix2) { + if (matrix1.length !== matrix2[0].length) { + throw new Error(_t("Cannot multiply matrices : incompatible matrices size.")); + } + const rowsM1 = matrix1[0].length; + const colsM2 = matrix2.length; + const n = matrix1.length; + const result = Array(colsM2); + for (let col = 0; col < colsM2; col++) { + result[col] = Array(rowsM1); + for (let row = 0; row < rowsM1; row++) { + let sum = 0; + for (let k = 0; k < n; k++) { + sum += matrix1[k][row] * matrix2[col][k]; + } + result[col][row] = sum; + } + } + return result; + } + /** + * Return the input if it's a scalar or the first element of the input if it's a matrix. + */ + function toScalar(matrix) { + if (!isMatrix(matrix)) { + return matrix; + } + if (matrix.length !== 1 || matrix[0].length !== 1) { + throw new Error("toScalar: matrix should be a scalar or a 1x1 matrix"); + } + return matrix[0][0]; + } + + // ----------------------------------------------------------------------------- + // ARRAY_CONSTRAIN + // ----------------------------------------------------------------------------- + const ARRAY_CONSTRAIN = { + description: _t("Returns a result array constrained to a specific width and height."), + args: [ + arg("input_range (any, range)", _t("The range to constrain.")), + arg("rows (number)", _t("The number of rows in the constrained array.")), + arg("columns (number)", _t("The number of columns in the constrained array.")), + ], + returns: ["RANGE"], + computeValueAndFormat: function (array, rows, columns) { + const _array = toMatrix(array); + const _rowsArg = toInteger(rows?.value, this.locale); + const _columnsArg = toInteger(columns?.value, this.locale); + assertPositive(_t("The rows argument (%s) must be strictly positive.", _rowsArg.toString()), _rowsArg); + assertPositive(_t("The columns argument (%s) must be strictly positive.", _rowsArg.toString()), _columnsArg); + const _nbRows = Math.min(_rowsArg, _array[0].length); + const _nbColumns = Math.min(_columnsArg, _array.length); + return generateMatrix(_nbColumns, _nbRows, (col, row) => _array[col][row]); + }, + isExported: false, + }; + // ----------------------------------------------------------------------------- + // CHOOSECOLS + // ----------------------------------------------------------------------------- + const CHOOSECOLS = { + description: _t("Creates a new array from the selected columns in the existing range."), + args: [ + arg("array (any, range)", _t("The array that contains the columns to be returned.")), + arg("col_num (number, range)", _t("The first column index of the columns to be returned.")), + arg("col_num2 (number, range, repeating)", _t("The columns indexes of the columns to be returned.")), + ], + returns: ["RANGE"], + computeValueAndFormat: function (array, ...columns) { + const _array = toMatrix(array); + const _columns = flattenRowFirst(columns, (item) => toInteger(item?.value, this.locale)); + assert(() => _columns.every((col) => col > 0 && col <= _array.length), _t("The columns arguments must be between 1 and %s (got %s).", _array.length.toString(), (_columns.find((col) => col <= 0 || col > _array.length) || 0).toString())); + const result = Array(_columns.length); + for (let col = 0; col < _columns.length; col++) { + const colIndex = _columns[col] - 1; // -1 because columns arguments are 1-indexed + result[col] = _array[colIndex]; + } + return result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // CHOOSEROWS + // ----------------------------------------------------------------------------- + const CHOOSEROWS = { + description: _t("Creates a new array from the selected rows in the existing range."), + args: [ + arg("array (any, range)", _t("The array that contains the rows to be returned.")), + arg("row_num (number, range)", _t("The first row index of the rows to be returned.")), + arg("row_num2 (number, range, repeating)", _t("The rows indexes of the rows to be returned.")), + ], + returns: ["RANGE"], + computeValueAndFormat: function (array, ...rows) { + const _array = toMatrix(array); + const _rows = flattenRowFirst(rows, (item) => toInteger(item?.value, this.locale)); + const _nbColumns = _array.length; + assert(() => _rows.every((row) => row > 0 && row <= _array[0].length), _t("The rows arguments must be between 1 and %s (got %s).", _array[0].length.toString(), (_rows.find((row) => row <= 0 || row > _array[0].length) || 0).toString())); + return generateMatrix(_nbColumns, _rows.length, (col, row) => _array[col][_rows[row] - 1]); // -1 because rows arguments are 1-indexed + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // EXPAND + // ----------------------------------------------------------------------------- + const EXPAND = { + description: _t("Expands or pads an array to specified row and column dimensions."), + args: [ + arg("array (any, range)", _t("The array to expand.")), + arg("rows (number)", _t("The number of rows in the expanded array. If missing, rows will not be expanded.")), + arg("columns (number, optional)", _t("The number of columns in the expanded array. If missing, columns will not be expanded.")), + arg("pad_with (any, default=0)", _t("The value with which to pad.")), // @compatibility: on Excel, pad with #N/A + ], + returns: ["RANGE"], + computeValueAndFormat: function (arg, rows, columns, padWith = { value: 0 } // TODO : Replace with #N/A errors once it's supported + ) { + const _array = toMatrix(arg); + const _nbRows = toInteger(rows?.value, this.locale); + const _nbColumns = columns !== undefined ? toInteger(columns.value, this.local) : _array.length; + assert(() => _nbRows >= _array[0].length, _t("The rows arguments (%s) must be greater or equal than the number of rows of the array.", _nbRows.toString())); + assert(() => _nbColumns >= _array.length, _t("The columns arguments (%s) must be greater or equal than the number of columns of the array.", _nbColumns.toString())); + return generateMatrix(_nbColumns, _nbRows, (col, row) => col >= _array.length || row >= _array[col].length ? padWith : _array[col][row]); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // FLATTEN + // ----------------------------------------------------------------------------- + const FLATTEN = { + description: _t("Flattens all the values from one or more ranges into a single column."), + args: [ + arg("range (any, range)", _t("The first range to flatten.")), + arg("range2 (any, range, repeating)", _t("Additional ranges to flatten.")), + ], + returns: ["RANGE"], + computeValueAndFormat: function (...ranges) { + return [flattenRowFirst(ranges, (val) => (val === undefined ? { value: "" } : val))]; + }, + isExported: false, + }; + // ----------------------------------------------------------------------------- + // FREQUENCY + // ----------------------------------------------------------------------------- + const FREQUENCY = { + description: _t("Calculates the frequency distribution of a range."), + args: [ + arg("data (range)", _t("The array of ranges containing the values to be counted.")), + arg("classes (number, range)", _t("The range containing the set of classes.")), + ], + returns: ["RANGE"], + compute: function (data, classes) { + const _data = flattenRowFirst([data], (val) => val).filter((val) => typeof val === "number"); + const _classes = flattenRowFirst([classes], (val) => val).filter((val) => typeof val === "number"); + /** + * Returns the frequency distribution of the data in the classes, ie. the number of elements in the range + * between each classes. + * + * For example: + * - data = [1, 3, 2, 5, 4] + * - classes = [3, 5, 1] + * + * The result will be: + * - 2 ==> number of elements 1 > el >= 3 + * - 2 ==> number of elements 3 > el >= 5 + * - 1 ==> number of elements <= 1 + * - 0 ==> number of elements > 5 + * + * @compatibility: GSheet sort the input classes. We do the implemntation of Excel, where we kee the classes unsorted. + */ + const sortedClasses = _classes + .map((value, index) => ({ initialIndex: index, value, count: 0 })) + .sort((a, b) => a.value - b.value); + sortedClasses.push({ initialIndex: sortedClasses.length, value: Infinity, count: 0 }); + const sortedData = _data.sort((a, b) => a - b); + let index = 0; + for (const val of sortedData) { + while (val > sortedClasses[index].value && index < sortedClasses.length - 1) { + index++; + } + sortedClasses[index].count++; + } + const result = sortedClasses + .sort((a, b) => a.initialIndex - b.initialIndex) + .map((val) => val.count); + return [result]; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // HSTACK + // ----------------------------------------------------------------------------- + const HSTACK = { + description: _t("Appends ranges horizontally and in sequence to return a larger array."), + args: [ + arg("range1 (any, range)", _t("The first range to be appended.")), + arg("range2 (any, range, repeating)", _t("Additional ranges to add to range1.")), + ], + returns: ["RANGE"], + computeValueAndFormat: function (...ranges) { + const nbRows = Math.max(...ranges.map((r) => r?.[0]?.length ?? 0)); + const result = []; + for (const range of ranges) { + const _range = toMatrix(range); + for (let col = 0; col < _range.length; col++) { + //TODO: fill with #N/A for unavailable values instead of zeroes + const array = Array(nbRows).fill({ value: null }); + for (let row = 0; row < _range[col].length; row++) { + array[row] = _range[col][row]; + } + result.push(array); + } + } + return result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MDETERM + // ----------------------------------------------------------------------------- + const MDETERM = { + description: _t("Returns the matrix determinant of a square matrix."), + args: [ + arg("square_matrix (number, range)", _t("An range with an equal number of rows and columns representing a matrix whose determinant will be calculated.")), + ], + returns: ["NUMBER"], + compute: function (matrix) { + const _matrix = toMatrix(matrix); + assertSquareMatrix(_t("The argument square_matrix must have the same number of columns and rows."), _matrix); + if (!isNumberMatrix(_matrix)) { + throw new Error(_t("The argument square_matrix must be a matrix of numbers.")); + } + const { determinant } = invertMatrix(_matrix); + return determinant; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MINVERSE + // ----------------------------------------------------------------------------- + const MINVERSE = { + description: _t("Returns the multiplicative inverse of a square matrix."), + args: [ + arg("square_matrix (number, range)", _t("An range with an equal number of rows and columns representing a matrix whose multiplicative inverse will be calculated.")), + ], + returns: ["RANGE"], + compute: function (matrix) { + const _matrix = toMatrix(matrix); + assertSquareMatrix(_t("The argument square_matrix must have the same number of columns and rows."), _matrix); + if (!isNumberMatrix(_matrix)) { + throw new Error(_t("The argument square_matrix must be a matrix of numbers.")); + } + const { inverted } = invertMatrix(_matrix); + if (!inverted) { + throw new Error(_t("The matrix is not invertible.")); + } + return inverted; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MMULT + // ----------------------------------------------------------------------------- + const MMULT = { + description: _t("Calculates the matrix product of two matrices."), + args: [ + arg("matrix1 (number, range)", _t("The first matrix in the matrix multiplication operation.")), + arg("matrix2 (number, range)", _t("The second matrix in the matrix multiplication operation.")), + ], + returns: ["RANGE"], + compute: function (matrix1, matrix2) { + const _matrix1 = toMatrix(matrix1); + const _matrix2 = toMatrix(matrix2); + assert(() => _matrix1.length === _matrix2[0].length, _t("In [[FUNCTION_NAME]], the number of columns of the first matrix (%s) must be equal to the \ + number of rows of the second matrix (%s).", _matrix1.length.toString(), _matrix2[0].length.toString())); + if (!isNumberMatrix(_matrix1) || !isNumberMatrix(_matrix2)) { + throw new Error(_t("The arguments matrix1 and matrix2 must be matrices of numbers.")); + } + return multiplyMatrices(_matrix1, _matrix2); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SUMPRODUCT + // ----------------------------------------------------------------------------- + const SUMPRODUCT = { + description: _t("Calculates the sum of the products of corresponding entries in equal-sized ranges."), + args: [ + arg("range1 (number, range)", _t("The first range whose entries will be multiplied with corresponding entries in the other ranges.")), + arg("range2 (number, range, repeating)", _t("The other range whose entries will be multiplied with corresponding entries in the other ranges.")), + ], + returns: ["NUMBER"], + compute: function (...args) { + assertSameDimensions(_t("All the ranges must have the same dimensions."), ...args); + const _args = args.map(toMatrix); + let result = 0; + for (let col = 0; col < _args[0].length; col++) { + for (let row = 0; row < _args[0][col].length; row++) { + if (!_args.every((range) => typeof range[col][row] === "number")) { + continue; + } + let product = 1; + for (const range of _args) { + product *= toNumber(range[col][row], this.locale); + } + result += product; + } + } + return result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SUMX2MY2 + // ----------------------------------------------------------------------------- + /** + * Return the sum of the callback applied to each pair of values in the two arrays. + * + * Ignore the pairs X,Y where one of the value isn't a number. Throw an error if no pair of numbers is found. + */ + function getSumXAndY(arrayX, arrayY, cb) { + assertSameDimensions("The arguments array_x and array_y must have the same dimensions.", arrayX, arrayY); + const _arrayX = toMatrix(arrayX); + const _arrayY = toMatrix(arrayY); + let validPairFound = false; + let result = 0; + for (const col in _arrayX) { + for (const row in _arrayX[col]) { + const arrayXValue = _arrayX[col][row]; + const arrayYValue = _arrayY[col][row]; + if (typeof arrayXValue !== "number" || typeof arrayYValue !== "number") { + continue; + } + validPairFound = true; + result += cb(arrayXValue, arrayYValue); + } + } + if (!validPairFound) { + throw new Error("The arguments array_x and array_y must contain at least one pair of numbers."); + } + return result; + } + const SUMX2MY2 = { + description: _t("Calculates the sum of the difference of the squares of the values in two array."), + args: [ + arg("array_x (number, range)", _t("The array or range of values whose squares will be reduced by the squares of corresponding entries in array_y and added together.")), + arg("array_y (number, range)", _t("The array or range of values whose squares will be subtracted from the squares of corresponding entries in array_x and added together.")), + ], + returns: ["NUMBER"], + compute: function (arrayX, arrayY) { + return getSumXAndY(arrayX, arrayY, (x, y) => x ** 2 - y ** 2); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SUMX2PY2 + // ----------------------------------------------------------------------------- + const SUMX2PY2 = { + description: _t("Calculates the sum of the sum of the squares of the values in two array."), + args: [ + arg("array_x (number, range)", _t("The array or range of values whose squares will be added to the squares of corresponding entries in array_y and added together.")), + arg("array_y (number, range)", _t("The array or range of values whose squares will be added to the squares of corresponding entries in array_x and added together.")), + ], + returns: ["NUMBER"], + compute: function (arrayX, arrayY) { + return getSumXAndY(arrayX, arrayY, (x, y) => x ** 2 + y ** 2); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SUMXMY2 + // ----------------------------------------------------------------------------- + const SUMXMY2 = { + description: _t("Calculates the sum of squares of the differences of values in two array."), + args: [ + arg("array_x (number, range)", _t("The array or range of values that will be reduced by corresponding entries in array_y, squared, and added together.")), + arg("array_y (number, range)", _t("The array or range of values that will be subtracted from corresponding entries in array_x, the result squared, and all such results added together.")), + ], + returns: ["NUMBER"], + compute: function (arrayX, arrayY) { + return getSumXAndY(arrayX, arrayY, (x, y) => (x - y) ** 2); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TOCOL + // ----------------------------------------------------------------------------- + const TO_COL_ROW_DEFAULT_IGNORE = 0; + const TO_COL_ROW_DEFAULT_SCAN = false; + const TO_COL_ROW_ARGS = [ + arg("array (any, range)", _t("The array which will be transformed.")), + arg(`ignore (number, default=${TO_COL_ROW_DEFAULT_IGNORE})`, _t("The control to ignore blanks and errors. 0 (default) is to keep all values, 1 is to ignore blanks, 2 is to ignore errors, and 3 is to ignore blanks and errors.")), + arg(`scan_by_column (number, default=${TO_COL_ROW_DEFAULT_SCAN})`, _t("Whether the array should be scanned by column. True scans the array by column and false (default) \ + scans the array by row.")), + ]; + const TOCOL = { + description: _t("Transforms a range of cells into a single column."), + args: TO_COL_ROW_ARGS, + returns: ["RANGE"], + computeValueAndFormat: function (array, ignore = { value: TO_COL_ROW_DEFAULT_IGNORE }, scanByColumn = { value: TO_COL_ROW_DEFAULT_SCAN }) { + const _array = toMatrix(array); + const _ignore = toInteger(ignore.value, this.locale); + const _scanByColumn = toBoolean(scanByColumn.value); + assert(() => _ignore >= 0 && _ignore <= 3, _t("Argument ignore must be between 0 and 3")); + // TODO : implement ignore value 2 (ignore error) & 3 (ignore blanks and errors) once we can have errors in + // the array w/o crashing + const result = (_scanByColumn ? _array : transposeMatrix(_array)) + .flat() + .filter((item) => (_ignore !== 1 && _ignore !== 3) || (item.value !== undefined && item.value !== null)); + if (result.length === 0) { + throw new NotAvailableError(_t("No results for the given arguments of TOCOL.")); + } + return [result]; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TOROW + // ----------------------------------------------------------------------------- + const TOROW = { + description: _t("Transforms a range of cells into a single row."), + args: TO_COL_ROW_ARGS, + returns: ["RANGE"], + computeValueAndFormat: function (array, ignore = { value: TO_COL_ROW_DEFAULT_IGNORE }, scanByColumn = { value: TO_COL_ROW_DEFAULT_SCAN }) { + const _array = toMatrix(array); + const _ignore = toInteger(ignore.value, this.locale); + const _scanByColumn = toBoolean(scanByColumn.value); + assert(() => _ignore >= 0 && _ignore <= 3, _t("Argument ignore must be between 0 and 3")); + // TODO : implement ignore value 2 (ignore error) & 3 (ignore blanks and errors) once we can have errors in + // the array w/o crashing + const result = (_scanByColumn ? _array : transposeMatrix(_array)) + .flat() + .filter((item) => (_ignore !== 1 && _ignore !== 3) || (item.value !== undefined && item.value !== null)) + .map((item) => [item]); + if (result.length === 0 || result[0].length === 0) { + throw new NotAvailableError(_t("No results for the given arguments of TOROW.")); + } + return result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TRANSPOSE + // ----------------------------------------------------------------------------- + const TRANSPOSE = { + description: _t("Transposes the rows and columns of a range."), + args: [arg("range (any, range)", _t("The range to be transposed."))], + returns: ["RANGE"], + computeValueAndFormat: function (arg) { + const _array = toMatrix(arg); + const nbColumns = _array[0].length; + const nbRows = _array.length; + return generateMatrix(nbColumns, nbRows, (col, row) => _array[row][col]); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // VSTACK + // ----------------------------------------------------------------------------- + const VSTACK = { + description: _t("Appends ranges vertically and in sequence to return a larger array."), + args: [ + arg("range1 (any, range)", _t("The first range to be appended.")), + arg("range2 (any, range, repeating)", _t("Additional ranges to add to range1.")), + ], + returns: ["RANGE"], + computeValueAndFormat: function (...ranges) { + const nbColumns = Math.max(...ranges.map((range) => toMatrix(range).length)); + const nbRows = ranges.reduce((acc, range) => acc + toMatrix(range)[0].length, 0); + const result = Array(nbColumns) + .fill([]) + .map(() => Array(nbRows).fill({ value: 0 })); // TODO fill with #N/A + let currentRow = 0; + for (const range of ranges) { + const _array = toMatrix(range); + for (let col = 0; col < _array.length; col++) { + for (let row = 0; row < _array[col].length; row++) { + result[col][currentRow + row] = _array[col][row]; + } + } + currentRow += _array[0].length; + } + return result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // WRAPCOLS + // ----------------------------------------------------------------------------- + const WRAPCOLS = { + description: _t("Wraps the provided row or column of cells by columns after a specified number of elements to form a new array."), + args: [ + arg("range (any, range)", _t("The range to wrap.")), + arg("wrap_count (number)", _t("The maximum number of cells for each column, rounded down to the nearest whole number.")), + arg("pad_with (any, default=0)", // TODO : replace with #N/A + _t("The value with which to fill the extra cells in the range.")), + ], + returns: ["RANGE"], + computeValueAndFormat: function (range, wrapCount, padWith = { value: 0 }) { + const _array = toMatrix(range); + const nbRows = toInteger(wrapCount?.value, this.locale); + assertSingleColOrRow(_t("Argument range must be a single row or column."), _array); + const array = _array.flat(); + const nbColumns = Math.ceil(array.length / nbRows); + return generateMatrix(nbColumns, nbRows, (col, row) => { + const index = col * nbRows + row; + return index < array.length ? array[index] : padWith; + }); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // WRAPROWS + // ----------------------------------------------------------------------------- + const WRAPROWS = { + description: _t("Wraps the provided row or column of cells by rows after a specified number of elements to form a new array."), + args: [ + arg("range (any, range)", _t("The range to wrap.")), + arg("wrap_count (number)", _t("The maximum number of cells for each row, rounded down to the nearest whole number.")), + arg("pad_with (any, default=0)", // TODO : replace with #N/A + _t("The value with which to fill the extra cells in the range.")), + ], + returns: ["RANGE"], + computeValueAndFormat: function (range, wrapCount, padWith = { value: 0 }) { + const _array = toMatrix(range); + const nbColumns = toInteger(wrapCount?.value, this.locale); + assertSingleColOrRow(_t("Argument range must be a single row or column."), _array); + const array = _array.flat(); + const nbRows = Math.ceil(array.length / nbColumns); + return generateMatrix(nbColumns, nbRows, (col, row) => { + const index = row * nbColumns + col; + return index < array.length ? array[index] : padWith; + }); + }, + isExported: true, + }; + + var array = /*#__PURE__*/Object.freeze({ + __proto__: null, + ARRAY_CONSTRAIN: ARRAY_CONSTRAIN, + CHOOSECOLS: CHOOSECOLS, + CHOOSEROWS: CHOOSEROWS, + EXPAND: EXPAND, + FLATTEN: FLATTEN, + FREQUENCY: FREQUENCY, + HSTACK: HSTACK, + MDETERM: MDETERM, + MINVERSE: MINVERSE, + MMULT: MMULT, + SUMPRODUCT: SUMPRODUCT, + SUMX2MY2: SUMX2MY2, + SUMX2PY2: SUMX2PY2, + SUMXMY2: SUMXMY2, + TOCOL: TOCOL, + TOROW: TOROW, + TRANSPOSE: TRANSPOSE, + VSTACK: VSTACK, + WRAPCOLS: WRAPCOLS, + WRAPROWS: WRAPROWS + }); + + // ----------------------------------------------------------------------------- + // FORMAT.LARGE.NUMBER + // ----------------------------------------------------------------------------- + const FORMAT_LARGE_NUMBER = { + description: _t("Apply a large number format"), + args: [ + arg("value (number)", _t("The number.")), + arg("unit (string, optional)", _t("The formatting unit. Use 'k', 'm', or 'b' to force the unit")), + ], + returns: ["NUMBER"], + computeFormat: function (arg, unit) { + const value = Math.abs(toNumber(arg?.value, this.locale)); + const format = arg?.format; + if (unit !== undefined) { + const postFix = unit?.value; + switch (postFix) { + case "k": + return createLargeNumberFormat(format, 1e3, "k", this.locale); + case "m": + return createLargeNumberFormat(format, 1e6, "m", this.locale); + case "b": + return createLargeNumberFormat(format, 1e9, "b", this.locale); + default: + throw new Error(_t("The formatting unit should be 'k', 'm' or 'b'.")); + } + } + if (value < 1e5) { + return createLargeNumberFormat(format, 0, "", this.locale); + } + else if (value < 1e8) { + return createLargeNumberFormat(format, 1e3, "k", this.locale); + } + else if (value < 1e11) { + return createLargeNumberFormat(format, 1e6, "m", this.locale); + } + return createLargeNumberFormat(format, 1e9, "b", this.locale); + }, + compute: function (value) { + return toNumber(value, this.locale); + }, + }; + + var misc = /*#__PURE__*/Object.freeze({ + __proto__: null, + FORMAT_LARGE_NUMBER: FORMAT_LARGE_NUMBER + }); + + const DEFAULT_FACTOR = 1; + const DEFAULT_MODE = 0; + const DEFAULT_PLACES = 0; + const DEFAULT_SIGNIFICANCE = 1; + const DECIMAL_REPRESENTATION = /^-?[a-z0-9]+$/i; + // ----------------------------------------------------------------------------- + // ABS + // ----------------------------------------------------------------------------- + const ABS = { + description: _t("Absolute value of a number."), + args: [arg("value (number)", _t("The number of which to return the absolute value."))], + returns: ["NUMBER"], + compute: function (value) { + return Math.abs(toNumber(value, this.locale)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ACOS + // ----------------------------------------------------------------------------- + const ACOS = { + description: _t("Inverse cosine of a value, in radians."), + args: [ + arg("value (number)", _t("The value for which to calculate the inverse cosine. Must be between -1 and 1, inclusive.")), + ], + returns: ["NUMBER"], + compute: function (value) { + const _value = toNumber(value, this.locale); + assert(() => Math.abs(_value) <= 1, _t("The value (%s) must be between -1 and 1 inclusive.", _value.toString())); + return Math.acos(_value); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ACOSH + // ----------------------------------------------------------------------------- + const ACOSH = { + description: _t("Inverse hyperbolic cosine of a number."), + args: [ + arg("value (number)", _t("The value for which to calculate the inverse hyperbolic cosine. Must be greater than or equal to 1.")), + ], + returns: ["NUMBER"], + compute: function (value) { + const _value = toNumber(value, this.locale); + assert(() => _value >= 1, _t("The value (%s) must be greater than or equal to 1.", _value.toString())); + return Math.acosh(_value); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ACOT + // ----------------------------------------------------------------------------- + const ACOT = { + description: _t("Inverse cotangent of a value."), + args: [arg("value (number)", _t("The value for which to calculate the inverse cotangent."))], + returns: ["NUMBER"], + compute: function (value) { + const _value = toNumber(value, this.locale); + const sign = Math.sign(_value) || 1; + // ACOT has two possible configurations: + // @compatibility Excel: return Math.PI / 2 - Math.atan(toNumber(_value, this.locale)); + // @compatibility Google: return sign * Math.PI / 2 - Math.atan(toNumber(_value, this.locale)); + return (sign * Math.PI) / 2 - Math.atan(_value); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ACOTH + // ----------------------------------------------------------------------------- + const ACOTH = { + description: _t("Inverse hyperbolic cotangent of a value."), + args: [ + arg("value (number)", _t("The value for which to calculate the inverse hyperbolic cotangent. Must not be between -1 and 1, inclusive.")), + ], + returns: ["NUMBER"], + compute: function (value) { + const _value = toNumber(value, this.locale); + assert(() => Math.abs(_value) > 1, _t("The value (%s) cannot be between -1 and 1 inclusive.", _value.toString())); + return Math.log((_value + 1) / (_value - 1)) / 2; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ASIN + // ----------------------------------------------------------------------------- + const ASIN = { + description: _t("Inverse sine of a value, in radians."), + args: [ + arg("value (number)", _t("The value for which to calculate the inverse sine. Must be between -1 and 1, inclusive.")), + ], + returns: ["NUMBER"], + compute: function (value) { + const _value = toNumber(value, this.locale); + assert(() => Math.abs(_value) <= 1, _t("The value (%s) must be between -1 and 1 inclusive.", _value.toString())); + return Math.asin(_value); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ASINH + // ----------------------------------------------------------------------------- + const ASINH = { + description: _t("Inverse hyperbolic sine of a number."), + args: [ + arg("value (number)", _t("The value for which to calculate the inverse hyperbolic sine.")), + ], + returns: ["NUMBER"], + compute: function (value) { + return Math.asinh(toNumber(value, this.locale)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ATAN + // ----------------------------------------------------------------------------- + const ATAN = { + description: _t("Inverse tangent of a value, in radians."), + args: [arg("value (number)", _t("The value for which to calculate the inverse tangent."))], + returns: ["NUMBER"], + compute: function (value) { + return Math.atan(toNumber(value, this.locale)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ATAN2 + // ----------------------------------------------------------------------------- + const ATAN2 = { + description: _t("Angle from the X axis to a point (x,y), in radians."), + args: [ + arg("x (number)", _t("The x coordinate of the endpoint of the line segment for which to calculate the angle from the x-axis.")), + arg("y (number)", _t("The y coordinate of the endpoint of the line segment for which to calculate the angle from the x-axis.")), + ], + returns: ["NUMBER"], + compute: function (x, y) { + const _x = toNumber(x, this.locale); + const _y = toNumber(y, this.locale); + assert(() => _x !== 0 || _y !== 0, _t("Function [[FUNCTION_NAME]] caused a divide by zero error.")); + return Math.atan2(_y, _x); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ATANH + // ----------------------------------------------------------------------------- + const ATANH = { + description: _t("Inverse hyperbolic tangent of a number."), + args: [ + arg("value (number)", _t("The value for which to calculate the inverse hyperbolic tangent. Must be between -1 and 1, exclusive.")), + ], + returns: ["NUMBER"], + compute: function (value) { + const _value = toNumber(value, this.locale); + assert(() => Math.abs(_value) < 1, _t("The value (%s) must be between -1 and 1 exclusive.", _value.toString())); + return Math.atanh(_value); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // CEILING + // ----------------------------------------------------------------------------- + const CEILING = { + description: _t("Rounds number up to nearest multiple of factor."), + args: [ + arg("value (number)", _t("The value to round up to the nearest integer multiple of factor.")), + arg(`factor (number, default=${DEFAULT_FACTOR})`, _t("The number to whose multiples value will be rounded.")), + ], + returns: ["NUMBER"], + computeFormat: (value) => value?.format, + compute: function (value, factor = DEFAULT_FACTOR) { + const _value = toNumber(value, this.locale); + const _factor = toNumber(factor, this.locale); + assert(() => _factor >= 0 || _value <= 0, _t("The factor (%s) must be positive when the value (%s) is positive.", _factor.toString(), _value.toString())); + return _factor ? Math.ceil(_value / _factor) * _factor : 0; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // CEILING.MATH + // ----------------------------------------------------------------------------- + const CEILING_MATH = { + description: _t("Rounds number up to nearest multiple of factor."), + args: [ + arg("number (number)", _t("The value to round up to the nearest integer multiple of significance.")), + arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded. The sign of significance will be ignored.")), + arg(`mode (number, default=${DEFAULT_MODE})`, _t("If number is negative, specifies the rounding direction. If 0 or blank, it is rounded towards zero. Otherwise, it is rounded away from zero.")), + ], + returns: ["NUMBER"], + computeFormat: (number) => number?.format, + compute: function (number, significance = DEFAULT_SIGNIFICANCE, mode = DEFAULT_MODE) { + let _significance = toNumber(significance, this.locale); + if (_significance === 0) { + return 0; + } + const _number = toNumber(number, this.locale); + _significance = Math.abs(_significance); + if (_number >= 0) { + return Math.ceil(_number / _significance) * _significance; + } + const _mode = toNumber(mode, this.locale); + if (_mode === 0) { + return -Math.floor(Math.abs(_number) / _significance) * _significance; + } + return -Math.ceil(Math.abs(_number) / _significance) * _significance; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // CEILING.PRECISE + // ----------------------------------------------------------------------------- + const CEILING_PRECISE = { + description: _t("Rounds number up to nearest multiple of factor."), + args: [ + arg("number (number)", _t("The value to round up to the nearest integer multiple of significance.")), + arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded.")), + ], + returns: ["NUMBER"], + computeFormat: (number) => number?.format, + compute: function (number, significance) { + return CEILING_MATH.compute.bind(this)(number, significance, 0); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COS + // ----------------------------------------------------------------------------- + const COS = { + description: _t("Cosine of an angle provided in radians."), + args: [arg("angle (number)", _t("The angle to find the cosine of, in radians."))], + returns: ["NUMBER"], + compute: function (angle) { + return Math.cos(toNumber(angle, this.locale)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COSH + // ----------------------------------------------------------------------------- + const COSH = { + description: _t("Hyperbolic cosine of any real number."), + args: [arg("value (number)", _t("Any real value to calculate the hyperbolic cosine of."))], + returns: ["NUMBER"], + compute: function (value) { + return Math.cosh(toNumber(value, this.locale)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COT + // ----------------------------------------------------------------------------- + const COT = { + description: _t("Cotangent of an angle provided in radians."), + args: [arg("angle (number)", _t("The angle to find the cotangent of, in radians."))], + returns: ["NUMBER"], + compute: function (angle) { + const _angle = toNumber(angle, this.locale); + assert(() => _angle !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.")); + return 1 / Math.tan(_angle); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COTH + // ----------------------------------------------------------------------------- + const COTH = { + description: _t("Hyperbolic cotangent of any real number."), + args: [arg("value (number)", _t("Any real value to calculate the hyperbolic cotangent of."))], + returns: ["NUMBER"], + compute: function (value) { + const _value = toNumber(value, this.locale); + assert(() => _value !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.")); + return 1 / Math.tanh(_value); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COUNTBLANK + // ----------------------------------------------------------------------------- + const COUNTBLANK = { + description: _t("Number of empty values."), + args: [ + arg("value1 (any, range)", _t("The first value or range in which to count the number of blanks.")), + arg("value2 (any, range, repeating)", _t("Additional values or ranges in which to count the number of blanks.")), + ], + returns: ["NUMBER"], + compute: function (...argsValues) { + return reduceAny(argsValues, (acc, a) => (a === null || a === undefined || a === "" ? acc + 1 : acc), 0); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COUNTIF + // ----------------------------------------------------------------------------- + const COUNTIF = { + description: _t("A conditional count across a range."), + args: [ + arg("range (range)", _t("The range that is tested against criterion.")), + arg("criterion (string)", _t("The pattern or test to apply to range.")), + ], + returns: ["NUMBER"], + compute: function (...argsValues) { + let count = 0; + visitMatchingRanges(argsValues, (i, j) => { + count += 1; + }, this.locale); + return count; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COUNTIFS + // ----------------------------------------------------------------------------- + const COUNTIFS = { + description: _t("Count values depending on multiple criteria."), + args: [ + arg("criteria_range1 (range)", _t("The range to check against criterion1.")), + arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1.")), + arg("criteria_range2 (any, range, repeating)", _t("Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.")), + arg("criterion2 (string, repeating)", _t("Additional criteria to check.")), + ], + returns: ["NUMBER"], + compute: function (...argsValues) { + let count = 0; + visitMatchingRanges(argsValues, (i, j) => { + count += 1; + }, this.locale); + return count; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COUNTUNIQUE + // ----------------------------------------------------------------------------- + function isDefined(value) { + switch (value) { + case undefined: + return false; + case "": + return false; + case null: + return false; + default: + return true; + } + } + const COUNTUNIQUE = { + description: _t("Counts number of unique values in a range."), + args: [ + arg("value1 (any, range)", _t("The first value or range to consider for uniqueness.")), + arg("value2 (any, range, repeating)", _t("Additional values or ranges to consider for uniqueness.")), + ], + returns: ["NUMBER"], + compute: function (...argsValues) { + return reduceAny(argsValues, (acc, a) => (isDefined(a) ? acc.add(a) : acc), new Set()).size; + }, + }; + // ----------------------------------------------------------------------------- + // COUNTUNIQUEIFS + // ----------------------------------------------------------------------------- + const COUNTUNIQUEIFS = { + description: _t("Counts number of unique values in a range, filtered by a set of criteria."), + args: [ + arg("range (range)", _t("The range of cells from which the number of unique values will be counted.")), + arg("criteria_range1 (range)", _t("The range of cells over which to evaluate criterion1.")), + arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.")), + arg("criteria_range2 (any, range, repeating)", _t("Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.")), + arg("criterion2 (string, repeating)", _t("The pattern or test to apply to criteria_range2.")), + ], + returns: ["NUMBER"], + compute: function (range, ...argsValues) { + let uniqueValues = new Set(); + visitMatchingRanges(argsValues, (i, j) => { + const value = range[i][j]; + if (isDefined(value)) { + uniqueValues.add(value); + } + }, this.locale); + return uniqueValues.size; + }, + }; + // ----------------------------------------------------------------------------- + // CSC + // ----------------------------------------------------------------------------- + const CSC = { + description: _t("Cosecant of an angle provided in radians."), + args: [arg("angle (number)", _t("The angle to find the cosecant of, in radians."))], + returns: ["NUMBER"], + compute: function (angle) { + const _angle = toNumber(angle, this.locale); + assert(() => _angle !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.")); + return 1 / Math.sin(_angle); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // CSCH + // ----------------------------------------------------------------------------- + const CSCH = { + description: _t("Hyperbolic cosecant of any real number."), + args: [arg("value (number)", _t("Any real value to calculate the hyperbolic cosecant of."))], + returns: ["NUMBER"], + compute: function (value) { + const _value = toNumber(value, this.locale); + assert(() => _value !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.")); + return 1 / Math.sinh(_value); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DECIMAL + // ----------------------------------------------------------------------------- + const DECIMAL = { + description: _t("Converts from another base to decimal."), + args: [ + arg("value (string)", _t("The number to convert.")), + arg(",base (number)", _t("The base to convert the value from.")), + ], + returns: ["NUMBER"], + compute: function (value, base) { + let _base = toNumber(base, this.locale); + _base = Math.floor(_base); + assert(() => 2 <= _base && _base <= 36, _t("The base (%s) must be between 2 and 36 inclusive.", _base.toString())); + const _value = toString(value); + if (_value === "") { + return 0; + } + /** + * @compatibility: on Google sheets, expects the parameter 'value' to be positive. + * Return error if 'value' is positive. + * Remove '-?' in the next regex to catch this error. + */ + assert(() => !!DECIMAL_REPRESENTATION.test(_value), _t("The value (%s) must be a valid base %s representation.", _value, _base.toString())); + const deci = parseInt(_value, _base); + assert(() => !isNaN(deci), _t("The value (%s) must be a valid base %s representation.", _value, _base.toString())); + return deci; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DEGREES + // ----------------------------------------------------------------------------- + const DEGREES = { + description: _t("Converts an angle value in radians to degrees."), + args: [arg("angle (number)", _t("The angle to convert from radians to degrees."))], + returns: ["NUMBER"], + compute: function (angle) { + return (toNumber(angle, this.locale) * 180) / Math.PI; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // EXP + // ----------------------------------------------------------------------------- + const EXP = { + description: _t("Euler's number, e (~2.718) raised to a power."), + args: [arg("value (number)", _t("The exponent to raise e."))], + returns: ["NUMBER"], + compute: function (value) { + return Math.exp(toNumber(value, this.locale)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // FLOOR + // ----------------------------------------------------------------------------- + const FLOOR = { + description: _t("Rounds number down to nearest multiple of factor."), + args: [ + arg("value (number)", _t("The value to round down to the nearest integer multiple of factor.")), + arg(`factor (number, default=${DEFAULT_FACTOR})`, _t("The number to whose multiples value will be rounded.")), + ], + returns: ["NUMBER"], + computeFormat: (value) => value?.format, + compute: function (value, factor = DEFAULT_FACTOR) { + const _value = toNumber(value, this.locale); + const _factor = toNumber(factor, this.locale); + assert(() => _factor >= 0 || _value <= 0, _t("The factor (%s) must be positive when the value (%s) is positive.", _factor.toString(), _value.toString())); + return _factor ? Math.floor(_value / _factor) * _factor : 0; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // FLOOR.MATH + // ----------------------------------------------------------------------------- + const FLOOR_MATH = { + description: _t("Rounds number down to nearest multiple of factor."), + args: [ + arg("number (number)", _t("The value to round down to the nearest integer multiple of significance.")), + arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded. The sign of significance will be ignored.")), + arg(`mode (number, default=${DEFAULT_MODE})`, _t("If number is negative, specifies the rounding direction. If 0 or blank, it is rounded away from zero. Otherwise, it is rounded towards zero.")), + ], + returns: ["NUMBER"], + computeFormat: (number) => number?.format, + compute: function (number, significance = DEFAULT_SIGNIFICANCE, mode = DEFAULT_MODE) { + let _significance = toNumber(significance, this.locale); + if (_significance === 0) { + return 0; + } + const _number = toNumber(number, this.locale); + _significance = Math.abs(_significance); + if (_number >= 0) { + return Math.floor(_number / _significance) * _significance; + } + const _mode = toNumber(mode, this.locale); + if (_mode === 0) { + return -Math.ceil(Math.abs(_number) / _significance) * _significance; + } + return -Math.floor(Math.abs(_number) / _significance) * _significance; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // FLOOR.PRECISE + // ----------------------------------------------------------------------------- + const FLOOR_PRECISE = { + description: _t("Rounds number down to nearest multiple of factor."), + args: [ + arg("number (number)", _t("The value to round down to the nearest integer multiple of significance.")), + arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded.")), + ], + returns: ["NUMBER"], + computeFormat: (number) => number?.format, + compute: function (number, significance = DEFAULT_SIGNIFICANCE) { + return FLOOR_MATH.compute.bind(this)(number, significance, 0); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ISEVEN + // ----------------------------------------------------------------------------- + const ISEVEN = { + description: _t("Whether the provided value is even."), + args: [arg("value (number)", _t("The value to be verified as even."))], + returns: ["BOOLEAN"], + compute: function (value) { + const _value = strictToNumber(value, this.locale); + return Math.floor(Math.abs(_value)) & 1 ? false : true; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ISO.CEILING + // ----------------------------------------------------------------------------- + const ISO_CEILING = { + description: _t("Rounds number up to nearest multiple of factor."), + args: [ + arg("number (number)", _t("The value to round up to the nearest integer multiple of significance.")), + arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded.")), + ], + returns: ["NUMBER"], + computeFormat: (number) => number?.format, + compute: function (number, significance = DEFAULT_SIGNIFICANCE) { + return CEILING_MATH.compute.bind(this)(number, significance, 0); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ISODD + // ----------------------------------------------------------------------------- + const ISODD = { + description: _t("Whether the provided value is even."), + args: [arg("value (number)", _t("The value to be verified as even."))], + returns: ["BOOLEAN"], + compute: function (value) { + const _value = strictToNumber(value, this.locale); + return Math.floor(Math.abs(_value)) & 1 ? true : false; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // LN + // ----------------------------------------------------------------------------- + const LN = { + description: _t("The logarithm of a number, base e (euler's number)."), + args: [arg("value (number)", _t("The value for which to calculate the logarithm, base e."))], + returns: ["NUMBER"], + compute: function (value) { + const _value = toNumber(value, this.locale); + assert(() => _value > 0, _t("The value (%s) must be strictly positive.", _value.toString())); + return Math.log(_value); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MOD + // ----------------------------------------------------------------------------- + const MOD = { + description: _t("Modulo (remainder) operator."), + args: [ + arg("dividend (number)", _t("The number to be divided to find the remainder.")), + arg("divisor (number)", _t("The number to divide by.")), + ], + returns: ["NUMBER"], + computeFormat: (dividend) => dividend?.format, + compute: function (dividend, divisor) { + const _divisor = toNumber(divisor, this.locale); + assert(() => _divisor !== 0, _t("The divisor must be different from 0.")); + const _dividend = toNumber(dividend, this.locale); + const modulus = _dividend % _divisor; + // -42 % 10 = -2 but we want 8, so need the code below + if ((modulus > 0 && _divisor < 0) || (modulus < 0 && _divisor > 0)) { + return modulus + _divisor; + } + return modulus; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MUNIT + // ----------------------------------------------------------------------------- + const MUNIT = { + description: _t("Returns a n x n unit matrix, where n is the input dimension."), + args: [ + arg("dimension (number)", _t("An integer specifying the dimension size of the unit matrix. It must be positive.")), + ], + returns: ["RANGE"], + compute: function (n) { + const _n = toInteger(n, this.locale); + assertPositive(_t("The argument dimension must be positive"), _n); + return getUnitMatrix(_n); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ODD + // ----------------------------------------------------------------------------- + const ODD = { + description: _t("Rounds a number up to the nearest odd integer."), + args: [arg("value (number)", _t("The value to round to the next greatest odd number."))], + returns: ["NUMBER"], + computeFormat: (number) => number?.format, + compute: function (value) { + const _value = toNumber(value, this.locale); + let temp = Math.ceil(Math.abs(_value)); + temp = temp & 1 ? temp : temp + 1; + return _value < 0 ? -temp : temp; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // PI + // ----------------------------------------------------------------------------- + const PI = { + description: _t("The number pi."), + args: [], + returns: ["NUMBER"], + compute: function () { + return Math.PI; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // POWER + // ----------------------------------------------------------------------------- + const POWER = { + description: _t("A number raised to a power."), + args: [ + arg("base (number)", _t("The number to raise to the exponent power.")), + arg("exponent (number)", _t("The exponent to raise base to.")), + ], + returns: ["NUMBER"], + computeFormat: (base) => base?.format, + compute: function (base, exponent) { + const _base = toNumber(base, this.locale); + const _exponent = toNumber(exponent, this.locale); + assert(() => _base >= 0 || Number.isInteger(_exponent), _t("The exponent (%s) must be an integer when the base is negative.", _exponent.toString())); + return Math.pow(_base, _exponent); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // PRODUCT + // ----------------------------------------------------------------------------- + const PRODUCT = { + description: _t("Result of multiplying a series of numbers together."), + args: [ + arg("factor1 (number, range)", _t("The first number or range to calculate for the product.")), + arg("factor2 (number, range, repeating)", _t("More numbers or ranges to calculate for the product.")), + ], + returns: ["NUMBER"], + computeFormat: (factor1) => { + return isMatrix(factor1) ? factor1[0][0]?.format : factor1?.format; + }, + compute: function (...factors) { + let count = 0; + let acc = 1; + for (let n of factors) { + if (isMatrix(n)) { + for (let i of n) { + for (let j of i) { + if (typeof j === "number") { + acc *= j; + count += 1; + } + } + } + } + else if (n !== null && n !== undefined) { + acc *= strictToNumber(n, this.locale); + count += 1; + } + } + if (count === 0) { + return 0; + } + return acc; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // RAND + // ----------------------------------------------------------------------------- + const RAND = { + description: _t("A random number between 0 inclusive and 1 exclusive."), + args: [], + returns: ["NUMBER"], + compute: function () { + return Math.random(); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // RANDARRAY + // ----------------------------------------------------------------------------- + const RANDARRAY = { + description: _t("Returns a grid of random numbers between 0 inclusive and 1 exclusive."), + args: [ + arg("rows (number, default=1)", _t("The number of rows to be returned.")), + arg("columns (number, default=1)", _t("The number of columns to be returned.")), + arg("min (number, default=0)", _t("The minimum number you would like returned.")), + arg("max (number, default=1)", _t("The maximum number you would like returned.")), + arg("whole_number (number, default=FALSE)", _t("Return a whole number or a decimal value.")), + ], + returns: ["RANGE"], + compute: function (rows = 1, columns = 1, min = 0, max = 1, whole_number = false) { + const _cols = toInteger(columns, this.locale); + const _rows = toInteger(rows, this.locale); + const _min = toNumber(min, this.locale); + const _max = toNumber(max, this.locale); + const _whole_number = toBoolean(whole_number); + assertPositive(_t("The number columns (%s) must be positive.", _cols.toString()), _cols); + assertPositive(_t("The number rows (%s) must be positive.", _rows.toString()), _rows); + assert(() => _min <= _max, _t("The maximum (%s) must be greater than or equal to the minimum (%s).", _max.toString(), _min.toString())); + if (_whole_number) { + assert(() => Number.isInteger(_min) && Number.isInteger(_max), _t("The maximum (%s) and minimum (%s) must be integers when whole_number is TRUE.", _max.toString(), _min.toString())); + } + const result = Array(_cols); + for (let col = 0; col < _cols; col++) { + result[col] = Array(_rows); + for (let row = 0; row < _rows; row++) { + if (!_whole_number) { + result[col][row] = _min + Math.random() * (_max - _min); + } + else { + result[col][row] = Math.floor(Math.random() * (_max - _min + 1) + _min); + } + } + } + return result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // RANDBETWEEN + // ----------------------------------------------------------------------------- + const RANDBETWEEN = { + description: _t("Random integer between two values, inclusive."), + args: [ + arg("low (number)", _t("The low end of the random range.")), + arg("high (number)", _t("The high end of the random range.")), + ], + returns: ["NUMBER"], + computeFormat: (low) => low?.format, + compute: function (low, high) { + let _low = toNumber(low, this.locale); + if (!Number.isInteger(_low)) { + _low = Math.ceil(_low); + } + let _high = toNumber(high, this.locale); + if (!Number.isInteger(_high)) { + _high = Math.floor(_high); + } + assert(() => _low <= _high, _t("The high (%s) must be greater than or equal to the low (%s).", _high.toString(), _low.toString())); + return _low + Math.ceil((_high - _low + 1) * Math.random()) - 1; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ROUND + // ----------------------------------------------------------------------------- + const ROUND = { + description: _t("Rounds a number according to standard rules."), + args: [ + arg("value (number)", _t("The value to round to places number of places.")), + arg(`places (number, default=${DEFAULT_PLACES})`, _t("The number of decimal places to which to round.")), + ], + returns: ["NUMBER"], + computeFormat: (value) => value?.format, + compute: function (value, places = DEFAULT_PLACES) { + const _value = toNumber(value, this.locale); + let _places = toNumber(places, this.locale); + const absValue = Math.abs(_value); + let tempResult; + if (_places === 0) { + tempResult = Math.round(absValue); + } + else { + if (!Number.isInteger(_places)) { + _places = Math.trunc(_places); + } + tempResult = Math.round(absValue * Math.pow(10, _places)) / Math.pow(10, _places); + } + return _value >= 0 ? tempResult : -tempResult; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ROUNDDOWN + // ----------------------------------------------------------------------------- + const ROUNDDOWN = { + description: _t("Rounds down a number."), + args: [ + arg("value (number)", _t("The value to round to places number of places, always rounding down.")), + arg(`places (number, default=${DEFAULT_PLACES})`, _t("The number of decimal places to which to round.")), + ], + returns: ["NUMBER"], + computeFormat: (value) => value?.format, + compute: function (value, places = DEFAULT_PLACES) { + const _value = toNumber(value, this.locale); + let _places = toNumber(places, this.locale); + const absValue = Math.abs(_value); + let tempResult; + if (_places === 0) { + tempResult = Math.floor(absValue); + } + else { + if (!Number.isInteger(_places)) { + _places = Math.trunc(_places); + } + tempResult = Math.floor(absValue * Math.pow(10, _places)) / Math.pow(10, _places); + } + return _value >= 0 ? tempResult : -tempResult; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ROUNDUP + // ----------------------------------------------------------------------------- + const ROUNDUP = { + description: _t("Rounds up a number."), + args: [ + arg("value (number)", _t("The value to round to places number of places, always rounding up.")), + arg(`places (number, default=${DEFAULT_PLACES})`, _t("The number of decimal places to which to round.")), + ], + returns: ["NUMBER"], + computeFormat: (value) => value?.format, + compute: function (value, places = DEFAULT_PLACES) { + const _value = toNumber(value, this.locale); + let _places = toNumber(places, this.locale); + const absValue = Math.abs(_value); + let tempResult; + if (_places === 0) { + tempResult = Math.ceil(absValue); + } + else { + if (!Number.isInteger(_places)) { + _places = Math.trunc(_places); + } + tempResult = Math.ceil(absValue * Math.pow(10, _places)) / Math.pow(10, _places); + } + return _value >= 0 ? tempResult : -tempResult; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SEC + // ----------------------------------------------------------------------------- + const SEC = { + description: _t("Secant of an angle provided in radians."), + args: [arg("angle (number)", _t("The angle to find the secant of, in radians."))], + returns: ["NUMBER"], + compute: function (angle) { + return 1 / Math.cos(toNumber(angle, this.locale)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SECH + // ----------------------------------------------------------------------------- + const SECH = { + description: _t("Hyperbolic secant of any real number."), + args: [arg("value (number)", _t("Any real value to calculate the hyperbolic secant of."))], + returns: ["NUMBER"], + compute: function (value) { + return 1 / Math.cosh(toNumber(value, this.locale)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SIN + // ----------------------------------------------------------------------------- + const SIN = { + description: _t("Sine of an angle provided in radians."), + args: [arg("angle (number)", _t("The angle to find the sine of, in radians."))], + returns: ["NUMBER"], + compute: function (angle) { + return Math.sin(toNumber(angle, this.locale)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SINH + // ----------------------------------------------------------------------------- + const SINH = { + description: _t("Hyperbolic sine of any real number."), + args: [arg("value (number)", _t("Any real value to calculate the hyperbolic sine of."))], + returns: ["NUMBER"], + compute: function (value) { + return Math.sinh(toNumber(value, this.locale)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SQRT + // ----------------------------------------------------------------------------- + const SQRT = { + description: _t("Positive square root of a positive number."), + args: [arg("value (number)", _t("The number for which to calculate the positive square root."))], + returns: ["NUMBER"], + computeFormat: (value) => value?.format, + compute: function (value) { + const _value = toNumber(value, this.locale); + assert(() => _value >= 0, _t("The value (%s) must be positive or null.", _value.toString())); + return Math.sqrt(_value); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SUM + // ----------------------------------------------------------------------------- + const SUM = { + description: _t("Sum of a series of numbers and/or cells."), + args: [ + arg("value1 (number, range)", _t("The first number or range to add together.")), + arg("value2 (number, range, repeating)", _t("Additional numbers or ranges to add to value1.")), + ], + returns: ["NUMBER"], + computeFormat: (value1) => { + return isMatrix(value1) ? value1[0][0]?.format : value1?.format; + }, + compute: function (...values) { + return reduceNumbers(values, (acc, a) => acc + a, 0, this.locale); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SUMIF + // ----------------------------------------------------------------------------- + const SUMIF = { + description: _t("A conditional sum across a range."), + args: [ + arg("criteria_range (range)", _t("The range which is tested against criterion.")), + arg("criterion (string)", _t("The pattern or test to apply to range.")), + arg("sum_range (range, default=criteria_range)", _t("The range to be summed, if different from range.")), + ], + returns: ["NUMBER"], + compute: function (criteriaRange, criterion, sumRange) { + if (sumRange === undefined) { + sumRange = criteriaRange; + } + let sum = 0; + visitMatchingRanges([criteriaRange, criterion], (i, j) => { + const value = sumRange[i][j]; + if (typeof value === "number") { + sum += value; + } + }, this.locale); + return sum; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SUMIFS + // ----------------------------------------------------------------------------- + const SUMIFS = { + description: _t("Sums a range depending on multiple criteria."), + args: [ + arg("sum_range (range)", _t("The range to sum.")), + arg("criteria_range1 (range)", _t("The range to check against criterion1.")), + arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1.")), + arg("criteria_range2 (any, range, repeating)", _t("Additional ranges to check.")), + arg("criterion2 (string, repeating)", _t("Additional criteria to check.")), + ], + returns: ["NUMBER"], + compute: function (sumRange, ...criters) { + let sum = 0; + visitMatchingRanges(criters, (i, j) => { + const value = sumRange[i][j]; + if (typeof value === "number") { + sum += value; + } + }, this.locale); + return sum; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TAN + // ----------------------------------------------------------------------------- + const TAN = { + description: _t("Tangent of an angle provided in radians."), + args: [arg("angle (number)", _t("The angle to find the tangent of, in radians."))], + returns: ["NUMBER"], + compute: function (angle) { + return Math.tan(toNumber(angle, this.locale)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TANH + // ----------------------------------------------------------------------------- + const TANH = { + description: _t("Hyperbolic tangent of any real number."), + args: [arg("value (number)", _t("Any real value to calculate the hyperbolic tangent of."))], + returns: ["NUMBER"], + compute: function (value) { + return Math.tanh(toNumber(value, this.locale)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TRUNC + // ----------------------------------------------------------------------------- + const TRUNC = { + description: _t("Truncates a number."), + args: [ + arg("value (number)", _t("The value to be truncated.")), + arg(`places (number, default=${DEFAULT_PLACES})`, _t("The number of significant digits to the right of the decimal point to retain.")), + ], + returns: ["NUMBER"], + computeFormat: (value) => value?.format, + compute: function (value, places = DEFAULT_PLACES) { + const _value = toNumber(value, this.locale); + let _places = toNumber(places, this.locale); + if (_places === 0) { + return Math.trunc(_value); + } + if (!Number.isInteger(_places)) { + _places = Math.trunc(_places); + } + return Math.trunc(_value * Math.pow(10, _places)) / Math.pow(10, _places); + }, + isExported: true, + }; + + var math = /*#__PURE__*/Object.freeze({ + __proto__: null, + ABS: ABS, + ACOS: ACOS, + ACOSH: ACOSH, + ACOT: ACOT, + ACOTH: ACOTH, + ASIN: ASIN, + ASINH: ASINH, + ATAN: ATAN, + ATAN2: ATAN2, + ATANH: ATANH, + CEILING: CEILING, + CEILING_MATH: CEILING_MATH, + CEILING_PRECISE: CEILING_PRECISE, + COS: COS, + COSH: COSH, + COT: COT, + COTH: COTH, + COUNTBLANK: COUNTBLANK, + COUNTIF: COUNTIF, + COUNTIFS: COUNTIFS, + COUNTUNIQUE: COUNTUNIQUE, + COUNTUNIQUEIFS: COUNTUNIQUEIFS, + CSC: CSC, + CSCH: CSCH, + DECIMAL: DECIMAL, + DEGREES: DEGREES, + EXP: EXP, + FLOOR: FLOOR, + FLOOR_MATH: FLOOR_MATH, + FLOOR_PRECISE: FLOOR_PRECISE, + ISEVEN: ISEVEN, + ISODD: ISODD, + ISO_CEILING: ISO_CEILING, + LN: LN, + MOD: MOD, + MUNIT: MUNIT, + ODD: ODD, + PI: PI, + POWER: POWER, + PRODUCT: PRODUCT, + RAND: RAND, + RANDARRAY: RANDARRAY, + RANDBETWEEN: RANDBETWEEN, + ROUND: ROUND, + ROUNDDOWN: ROUNDDOWN, + ROUNDUP: ROUNDUP, + SEC: SEC, + SECH: SECH, + SIN: SIN, + SINH: SINH, + SQRT: SQRT, + SUM: SUM, + SUMIF: SUMIF, + SUMIFS: SUMIFS, + TAN: TAN, + TANH: TANH, + TRUNC: TRUNC + }); + + function assertSameNumberOfElements(...args) { + const dims = args[0].length; + args.forEach((arg, i) => assert(() => arg.length === dims, _t("[[FUNCTION_NAME]] has mismatched dimensions for argument %s (%s vs %s).", i.toString(), dims.toString(), arg.length.toString()))); + } + + function filterAndFlatData(dataY, dataX) { + const _flatDataY = []; + const _flatDataX = []; + let lenY = 0; + let lenX = 0; + visitAny([dataY], (y) => { + _flatDataY.push(y); + lenY += 1; + }); + visitAny([dataX], (x) => { + _flatDataX.push(x); + lenX += 1; + }); + assert(() => lenY === lenX, _t("[[FUNCTION_NAME]] has mismatched argument count %s vs %s.", lenY, lenX)); + const flatDataX = []; + const flatDataY = []; + for (let i = 0; i < lenY; i++) { + const valueY = _flatDataY[i]; + const valueX = _flatDataX[i]; + if (typeof valueY === "number" && typeof valueX === "number") { + flatDataY.push(valueY); + flatDataX.push(valueX); + } + } + return { flatDataX, flatDataY }; + } + // Note: dataY and dataX may not have the same dimension + function covariance(dataY, dataX, isSample) { + const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX); + const count = flatDataY.length; + assert(() => count !== 0 && (!isSample || count !== 1), _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.")); + let sumY = 0; + let sumX = 0; + for (let i = 0; i < count; i++) { + sumY += flatDataY[i]; + sumX += flatDataX[i]; + } + const averageY = sumY / count; + const averageX = sumX / count; + let acc = 0; + for (let i = 0; i < count; i++) { + acc += (flatDataY[i] - averageY) * (flatDataX[i] - averageX); + } + return acc / (count - (isSample ? 1 : 0)); + } + function variance(args, isSample, textAs0, locale) { + let count = 0; + let sum = 0; + const reduceFunction = textAs0 ? reduceNumbersTextAs0 : reduceNumbers; + sum = reduceFunction(args, (acc, a) => { + count += 1; + return acc + a; + }, 0, locale); + assert(() => count !== 0 && (!isSample || count !== 1), _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.")); + const average = sum / count; + return (reduceFunction(args, (acc, a) => acc + Math.pow(a - average, 2), 0, locale) / + (count - (isSample ? 1 : 0))); + } + function centile(data, percent, isInclusive, locale) { + const _percent = toNumber(percent, locale); + assert(() => (isInclusive ? 0 <= _percent && _percent <= 1 : 0 < _percent && _percent < 1), _t("Function [[FUNCTION_NAME]] parameter 2 value is out of range.")); + let sortedArray = []; + let index; + let count = 0; + visitAny(data, (d) => { + if (typeof d === "number") { + index = dichotomicSearch(sortedArray, d, "nextSmaller", "asc", sortedArray.length, (array, i) => array[i]); + sortedArray.splice(index + 1, 0, d); + count++; + } + }); + assert(() => count !== 0, _t("[[FUNCTION_NAME]] has no valid input data.")); + if (!isInclusive) { + // 2nd argument must be between 1/(n+1) and n/(n+1) with n the number of data + assert(() => 1 / (count + 1) <= _percent && _percent <= count / (count + 1), _t("Function [[FUNCTION_NAME]] parameter 2 value is out of range.")); + } + return percentile(sortedArray, _percent, isInclusive); + } + function prepareDataForRegression(X, Y, newX) { + const _X = X.length ? X : [range(1, Y.flat().length + 1)]; + const nVar = _X.length; + let _newX = newX.length ? newX : _X; + _newX = _newX.length === nVar ? transposeMatrix(_newX) : _newX; + return { _X, _newX }; + } + /* + * This function performs a linear regression on the data set. It returns an array with two elements. + * The first element is the slope, and the second element is the intercept. + * The linear regression line is: y = slope*x + intercept + * The function use the least squares method to find the best fit for the data set : + * see https://www.mathsisfun.com/data/least-squares-regression.html + * https://www.statology.org/standard-error-of-estimate/ + * https://agronomy4future.org/?p=16670 + * https://vitalflux.com/interpreting-f-statistics-in-linear-regression-formula-examples/ + * https://web.ist.utl.pt/~ist11038/compute/errtheory/,regression/regrthroughorigin.pdf + */ + function fullLinearRegression(X, Y, computeIntercept = true, verbose = false) { + const y = Y.flat(); + const n = y.length; + let { _X } = prepareDataForRegression(X, Y, []); + _X = _X.length === n ? transposeMatrix(_X) : _X.slice(); + assertSameNumberOfElements(_X[0], y); + const nVar = _X.length; + const nDeg = n - nVar - (computeIntercept ? 1 : 0); + const yMatrix = [y]; + const xMatrix = transposeMatrix(_X.reverse()); + let avgX = []; + for (let i = 0; i < nVar; i++) { + avgX.push(0); + if (computeIntercept) { + for (const xij of _X[i]) { + avgX[i] += xij; + } + avgX[i] /= n; + } + } + let avgY = 0; + if (computeIntercept) { + for (const yi of y) { + avgY += yi; + } + avgY /= n; + } + const redX = xMatrix.map((row) => row.map((value, i) => value - avgX[i])); + if (computeIntercept) { + xMatrix.forEach((row) => row.push(1)); + } + const coeffs = getLMSCoefficients(xMatrix, yMatrix); + if (!computeIntercept) { + coeffs.push([0]); + } + if (!verbose) { + return coeffs; + } + const dot1 = multiplyMatrices(redX, transposeMatrix(redX)); + const { inverted: dotInv } = invertMatrix(dot1); + if (dotInv === undefined) { + throw new Error(_t("Matrix is not invertible")); + } + let SSE = 0, SSR = 0; + for (let i = 0; i < n; i++) { + const yi = y[i] - avgY; + let temp = 0; + for (let j = 0; j < nVar; j++) { + const xi = redX[i][j]; + temp += xi * coeffs[j][0]; + } + const ei = yi - temp; + SSE += ei * ei; + SSR += temp * temp; + } + const RMSE = Math.sqrt(SSE / nDeg); + const r2 = SSR / (SSR + SSE); + const f_stat = SSR / nVar / (SSE / nDeg); + const deltaCoeffs = []; + for (let i = 0; i < nVar; i++) { + deltaCoeffs.push(RMSE * Math.sqrt(dotInv[i][i])); + } + if (computeIntercept) { + const dot2 = multiplyMatrices(dotInv, [avgX]); + const dot3 = multiplyMatrices(transposeMatrix([avgX]), dot2); + deltaCoeffs.push(RMSE * Math.sqrt(dot3[0][0] + 1 / y.length)); + } + const returned = [ + [coeffs[0][0], deltaCoeffs[0], r2, f_stat, SSR], + [coeffs[1][0], deltaCoeffs[1], RMSE, nDeg, SSE], + ]; + for (let i = 2; i < nVar; i++) { + returned.push([coeffs[i][0], deltaCoeffs[i], "", "", ""]); + } + if (computeIntercept) { + returned.push([coeffs[nVar][0], deltaCoeffs[nVar], "", "", ""]); + } + else { + returned.push([0, "", "", "", ""]); + } + return returned; + } + /* + This function performs a polynomial regression on the data set. It returns the coefficients of + the polynomial function that best fits the data set. + The polynomial function is: y = c0 + c1*x + c2*x^2 + ... + cn*x^n, where n is the order (degree) + of the polynomial. The returned coefficients are then in the form: [c0, c1, c2, ..., cn] + The function is based on the method of least squares : + see: https://mathworld.wolfram.com/LeastSquaresFittingPolynomial.html + */ + function polynomialRegression(flatY, flatX, order, intercept) { + assertSameNumberOfElements(flatX, flatY); + assert(() => order >= 1, _t("Function [[FUNCTION_NAME]] A regression of order less than 1 cannot be possible.")); + const yMatrix = [flatY]; + const xMatrix = flatX.map((x) => range(0, order).map((i) => Math.pow(x, order - i))); + if (intercept) { + xMatrix.forEach((row) => row.push(1)); + } + const coeffs = getLMSCoefficients(xMatrix, yMatrix); + if (!intercept) { + coeffs.push([0]); + } + return coeffs; + } + function getLMSCoefficients(xMatrix, yMatrix) { + const xMatrixT = transposeMatrix(xMatrix); + const dot1 = multiplyMatrices(xMatrix, xMatrixT); + const { inverted: dotInv } = invertMatrix(dot1); + if (dotInv === undefined) { + throw new Error(_t("Matrix is not invertible")); + } + const dot2 = multiplyMatrices(xMatrix, yMatrix); + return transposeMatrix(multiplyMatrices(dotInv, dot2)); + } + function evaluatePolynomial(coeffs, x, order) { + return coeffs.reduce((acc, coeff, i) => acc + coeff * Math.pow(x, order - i), 0); + } + function expM(M) { + return M.map((col) => col.map((cell) => Math.exp(cell))); + } + function logM(M) { + return M.map((col) => col.map((cell) => Math.log(cell))); + } + function predictLinearValues(Y, X, newX, computeIntercept) { + const { _X, _newX } = prepareDataForRegression(X, Y, newX); + const coeffs = fullLinearRegression(_X, Y, computeIntercept, false); + const nVar = coeffs.length - 1; + const newY = _newX.map((col) => { + let value = 0; + for (let i = 0; i < nVar; i++) { + value += coeffs[i][0] * col[nVar - i - 1]; + } + value += coeffs[nVar][0]; + return [value]; + }); + return newY.length === newX.length ? newY : transposeMatrix(newY); + } + // ----------------------------------------------------------------------------- + // AVEDEV + // ----------------------------------------------------------------------------- + const AVEDEV = { + description: _t("Average magnitude of deviations from mean."), + args: [ + arg("value1 (number, range)", _t("The first value or range of the sample.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the sample.")), + ], + returns: ["NUMBER"], + compute: function (...values) { + let count = 0; + const sum = reduceNumbers(values, (acc, a) => { + count += 1; + return acc + a; + }, 0, this.locale); + assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.")); + const average = sum / count; + return reduceNumbers(values, (acc, a) => acc + Math.abs(average - a), 0, this.locale) / count; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // AVERAGE + // ----------------------------------------------------------------------------- + const AVERAGE = { + description: _t("Numerical average value in a dataset, ignoring text."), + args: [ + arg("value1 (number, range)", _t("The first value or range to consider when calculating the average value.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to consider when calculating the average value.")), + ], + returns: ["NUMBER"], + computeFormat: (value1) => { + return isMatrix(value1) ? value1[0][0]?.format : value1?.format; + }, + compute: function (...values) { + let count = 0; + const sum = reduceNumbers(values, (acc, a) => { + count += 1; + return acc + a; + }, 0, this.locale); + assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.")); + return sum / count; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // AVERAGE.WEIGHTED + // ----------------------------------------------------------------------------- + const rangeError = _t("[[FUNCTION_NAME]] has mismatched range sizes."); + const negativeWeightError = _t("[[FUNCTION_NAME]] expects the weight to be positive or equal to 0."); + const AVERAGE_WEIGHTED = { + description: _t("Weighted average."), + args: [ + arg("values (number, range)", _t("Values to average.")), + arg("weights (number, range)", _t("Weights for each corresponding value.")), + arg("additional_values (number, range, repeating)", _t("Additional values to average.")), + arg("additional_weights (number, range, repeating)", _t("Additional weights.")), + ], + returns: ["NUMBER"], + computeFormat: (values) => { + return isMatrix(values) ? values[0][0]?.format : values?.format; + }, + compute: function (...values) { + let sum = 0; + let count = 0; + let value; + let weight; + assert(() => values.length % 2 === 0, _t("Wrong number of Argument[]. Expected an even number of Argument[].")); + for (let n = 0; n < values.length - 1; n += 2) { + value = values[n]; + weight = values[n + 1]; + // if (typeof value != typeof weight) { + // throw new Error(rangeError); + // } + if (isMatrix(value)) { + assert(() => isMatrix(weight), rangeError); + let dimColValue = value.length; + let dimLinValue = value[0].length; + assert(() => dimColValue === weight.length && dimLinValue === weight[0].length, rangeError); + for (let i = 0; i < dimColValue; i++) { + for (let j = 0; j < dimLinValue; j++) { + let subValue = value[i][j]; + let subWeight = weight[i][j]; + let subValueIsNumber = typeof subValue === "number"; + let subWeightIsNumber = typeof subWeight === "number"; + // typeof subValue or subWeight can be 'number' or 'undefined' + assert(() => subValueIsNumber === subWeightIsNumber, _t("[[FUNCTION_NAME]] expects number values.")); + if (subWeightIsNumber) { + assert(() => subWeight >= 0, negativeWeightError); + sum += subValue * subWeight; + count += subWeight; + } + } + } + } + else { + weight = toNumber(weight, this.locale); + value = toNumber(value, this.locale); + assert(() => weight >= 0, negativeWeightError); + sum += value * weight; + count += weight; + } + } + assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.")); + return sum / count; + }, + }; + // ----------------------------------------------------------------------------- + // AVERAGEA + // ----------------------------------------------------------------------------- + const AVERAGEA = { + description: _t("Numerical average value in a dataset."), + args: [ + arg("value1 (number, range)", _t("The first value or range to consider when calculating the average value.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to consider when calculating the average value.")), + ], + returns: ["NUMBER"], + computeFormat: (value1) => { + return isMatrix(value1) ? value1[0][0]?.format : value1?.format; + }, + compute: function (...values) { + let count = 0; + const sum = reduceNumbersTextAs0(values, (acc, a) => { + count += 1; + return acc + a; + }, 0, this.locale); + assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.")); + return sum / count; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // AVERAGEIF + // ----------------------------------------------------------------------------- + const AVERAGEIF = { + description: _t("Average of values depending on criteria."), + args: [ + arg("criteria_range (number, range)", _t("The range to check against criterion.")), + arg("criterion (string)", _t("The pattern or test to apply to criteria_range.")), + arg("average_range (number, range, default=criteria_range)", _t("The range to average. If not included, criteria_range is used for the average instead.")), + ], + returns: ["NUMBER"], + compute: function (criteriaRange, criterion, averageRange) { + const _criteriaRange = toMatrix(criteriaRange); + const _averageRange = averageRange === undefined ? _criteriaRange : toMatrix(averageRange); + let count = 0; + let sum = 0; + visitMatchingRanges([criteriaRange, criterion], (i, j) => { + const value = _averageRange[i][j]; + if (typeof value === "number") { + count += 1; + sum += value; + } + }, this.locale); + assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.")); + return sum / count; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // AVERAGEIFS + // ----------------------------------------------------------------------------- + const AVERAGEIFS = { + description: _t("Average of values depending on multiple criteria."), + args: [ + arg("average_range (range)", _t("The range to average.")), + arg("criteria_range1 (range)", _t("The range to check against criterion1.")), + arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1.")), + arg("criteria_range2 (any, range, repeating)", _t("Additional criteria_range and criterion to check.")), + arg("criterion2 (string, repeating)", _t("The pattern or test to apply to criteria_range2.")), + ], + returns: ["NUMBER"], + compute: function (averageRange, ...values) { + const _averageRange = toMatrix(averageRange); + let count = 0; + let sum = 0; + visitMatchingRanges(values, (i, j) => { + const value = _averageRange[i][j]; + if (typeof value === "number") { + count += 1; + sum += value; + } + }, this.locale); + assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error.")); + return sum / count; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COUNT + // ----------------------------------------------------------------------------- + const COUNT = { + description: _t("The number of numeric values in dataset."), + args: [ + arg("value1 (number, range)", _t("The first value or range to consider when counting.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to consider when counting.")), + ], + returns: ["NUMBER"], + compute: function (...values) { + let count = 0; + for (let n of values) { + if (isMatrix(n)) { + for (let i of n) { + for (let j of i) { + if (typeof j === "number") { + count += 1; + } + } + } + } + else if (typeof n !== "string" || + isNumber(n, this.locale) || + parseDateTime(n, this.locale)) { + count += 1; + } + } + return count; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COUNTA + // ----------------------------------------------------------------------------- + const COUNTA = { + description: _t("The number of values in a dataset."), + args: [ + arg("value1 (any, range)", _t("The first value or range to consider when counting.")), + arg("value2 (any, range, repeating)", _t("Additional values or ranges to consider when counting.")), + ], + returns: ["NUMBER"], + compute: function (...values) { + return reduceAny(values, (acc, a) => (a !== undefined && a !== null ? acc + 1 : acc), 0); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COVAR + // ----------------------------------------------------------------------------- + // Note: Unlike the VAR function which corresponds to the variance over a sample (VAR.S), + // the COVAR function corresponds to the covariance over an entire population (COVAR.P) + const COVAR = { + description: _t("The covariance of a dataset."), + args: [ + arg("data_y (any, range)", _t("The range representing the array or matrix of dependent data.")), + arg("data_x (any, range)", _t("The range representing the array or matrix of independent data.")), + ], + returns: ["NUMBER"], + compute: function (dataY, dataX) { + return covariance(dataY, dataX, false); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COVARIANCE.P + // ----------------------------------------------------------------------------- + const COVARIANCE_P = { + description: _t("The covariance of a dataset."), + args: [ + arg("data_y (any, range)", _t("The range representing the array or matrix of dependent data.")), + arg("data_x (any, range)", _t("The range representing the array or matrix of independent data.")), + ], + returns: ["NUMBER"], + compute: function (dataY, dataX) { + return covariance(dataY, dataX, false); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COVARIANCE.S + // ----------------------------------------------------------------------------- + const COVARIANCE_S = { + description: _t("The sample covariance of a dataset."), + args: [ + arg("data_y (any, range)", _t("The range representing the array or matrix of dependent data.")), + arg("data_x (any, range)", _t("The range representing the array or matrix of independent data.")), + ], + returns: ["NUMBER"], + compute: function (dataY, dataX) { + return covariance(dataY, dataX, true); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // FORECAST + // ----------------------------------------------------------------------------- + const FORECAST = { + description: _t("Calculates the expected y-value for a specified x based on a linear regression of a dataset."), + args: [ + arg("x (number, range)", _t("The value(s) on the x-axis to forecast.")), + arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), + arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), + ], + returns: ["NUMBER"], + compute: function (x, dataY, dataX) { + const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX); + return predictLinearValues([flatDataY], [flatDataX], matrixMap(toMatrix(x), (value) => toNumber(value, this.locale)), true); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // GROWTH + // ----------------------------------------------------------------------------- + const GROWTH = { + description: _t("Fits points to exponential growth trend."), + args: [ + arg("known_data_y (range)", _t("The array or range containing dependent (y) values that are already known, used to curve fit an ideal exponential growth curve.")), + arg("known_data_x (range, default={1;2;3;...})", _t("The values of the independent variable(s) corresponding with known_data_y.")), + arg("new_data_x (any, range, default=known_data_x)", _t("The data points to return the y values for on the ideal curve fit.")), + arg("b (boolean, default=TRUE)", _t("Given a general exponential form of y = b*m^x for a curve fit, calculates b if TRUE or forces b to be 1 and only calculates the m values if FALSE.")), + ], + returns: ["NUMBER"], + compute: function (knownDataY, knownDataX = [], newDataX = [], b = true) { + return expM(predictLinearValues(logM(tryCastAsNumberMatrix(knownDataY, "the first argument (known_data_y)")), tryCastAsNumberMatrix(knownDataX, "the second argument (known_data_x)"), tryCastAsNumberMatrix(newDataX, "the third argument (new_data_y)"), toBoolean(b))); + }, + }; + // ----------------------------------------------------------------------------- + // INTERCEPT + // ----------------------------------------------------------------------------- + const INTERCEPT = { + description: _t("Compute the intercept of the linear regression."), + args: [ + arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), + arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), + ], + returns: ["NUMBER"], + compute: function (dataY, dataX) { + const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX); + const [[], [intercept]] = fullLinearRegression([flatDataX], [flatDataY]); + return intercept; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // LARGE + // ----------------------------------------------------------------------------- + const LARGE = { + description: _t("Nth largest element from a data set."), + args: [ + arg("data (any, range)", _t("Array or range containing the dataset to consider.")), + arg("n (number)", _t("The rank from largest to smallest of the element to return.")), + ], + returns: ["NUMBER"], + computeValueAndFormat: function (data, n) { + const _n = Math.trunc(toNumber(n?.value, this.locale)); + let largests = []; + let index; + let count = 0; + visitAny([data], (d) => { + if (typeof d.value === "number") { + index = dichotomicSearch(largests, d.value, "nextSmaller", "asc", largests.length, (array, i) => array[i].value); + largests.splice(index + 1, 0, d); + count++; + if (count > _n) { + largests.shift(); + count--; + } + } + }); + const result = largests.shift(); + assert(() => result !== undefined, _t("[[FUNCTION_NAME]] has no valid input data.")); + assert(() => count >= _n, _t("Function [[FUNCTION_NAME]] parameter 2 value (%s) is out of range.", _n)); + return result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // LINEST + // ----------------------------------------------------------------------------- + const LINEST = { + description: _t("Compute the intercept of the linear regression."), + args: [ + arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), + arg("data_x (range, default={1;2;3;...})", _t("The range representing the array or matrix of independent data.")), + arg("calculate_b (boolean, default=TRUE)", _t("A flag specifying wheter to compute the slope or not")), + arg("verbose (boolean, default=FALSE)", _t("A flag specifying whether to return additional regression statistics or only the linear coefficients and the y-intercept")), + ], + returns: ["NUMBER"], + compute: function (dataY, dataX = [], calculateB = true, verbose = false) { + return fullLinearRegression(tryCastAsNumberMatrix(dataX, "the first argument (data_y)"), tryCastAsNumberMatrix(dataY, "the second argument (data_x)"), toBoolean(calculateB), toBoolean(verbose)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // LOGEST + // ----------------------------------------------------------------------------- + const LOGEST = { + description: _t("Compute the intercept of the linear regression."), + args: [ + arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), + arg("data_x (range, optional, default={1;2;3;...})", _t("The range representing the array or matrix of independent data.")), + arg("calculate_b (boolean, default=TRUE)", _t("A flag specifying wheter to compute the slope or not")), + arg("verbose (boolean, default=FALSE)", _t("A flag specifying whether to return additional regression statistics or only the linear coefficients and the y-intercept")), + ], + returns: ["NUMBER"], + compute: function (dataY, dataX = [], calculateB = true, verbose = false) { + const coeffs = fullLinearRegression(tryCastAsNumberMatrix(dataX, "the second argument (data_x)"), logM(tryCastAsNumberMatrix(dataY, "the first argument (data_y)")), toBoolean(calculateB), toBoolean(verbose)); + for (let i = 0; i < coeffs.length; i++) { + coeffs[i][0] = Math.exp(coeffs[i][0]); + } + return coeffs; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MATTHEWS + // ----------------------------------------------------------------------------- + const MATTHEWS = { + description: _t("Compute the Matthews correlation coefficient of a dataset."), + args: [ + arg("data_x (range)", _t("The range representing the array or matrix of observed data.")), + arg("data_y (range)", _t("The range representing the array or matrix of predicted data.")), + ], + returns: ["NUMBER"], + compute: function (dataX, dataY) { + const flatX = dataX.flat(); + const flatY = dataY.flat(); + assertSameNumberOfElements(flatX, flatY); + if (flatX.length === 0) { + throw new Error(_t("[[FUNCTION_NAME]] expects non-empty ranges for both parameters.")); + } + const n = flatX.length; + let trueN = 0, trueP = 0, falseP = 0, falseN = 0; + for (let i = 0; i < n; ++i) { + const isTrue1 = toBoolean(flatX[i]); + const isTrue2 = toBoolean(flatY[i]); + if (isTrue1 === isTrue2) { + if (isTrue1) { + trueP++; + } + else { + trueN++; + } + } + else { + if (isTrue1) { + falseN++; + } + else { + falseP++; + } + } + } + return ((trueP * trueN - falseP * falseN) / + Math.sqrt((trueP + falseP) * (trueP + falseN) * (trueN + falseP) * (trueN + falseN))); + }, + isExported: false, + }; + // ----------------------------------------------------------------------------- + // MAX + // ----------------------------------------------------------------------------- + const MAX = { + description: _t("Maximum value in a numeric dataset."), + args: [ + arg("value1 (number, range)", _t("The first value or range to consider when calculating the maximum value.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to consider when calculating the maximum value.")), + ], + returns: ["NUMBER"], + computeFormat: (value1) => { + return isMatrix(value1) ? value1[0][0]?.format : value1?.format; + }, + compute: function (...values) { + const result = reduceNumbers(values, (acc, a) => (acc < a ? a : acc), -Infinity, this.locale); + return result === -Infinity ? 0 : result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MAXA + // ----------------------------------------------------------------------------- + const MAXA = { + description: _t("Maximum numeric value in a dataset."), + args: [ + arg("value1 (any, range)", _t("The first value or range to consider when calculating the maximum value.")), + arg("value2 (any, range, repeating)", _t("Additional values or ranges to consider when calculating the maximum value.")), + ], + returns: ["NUMBER"], + computeFormat: (value1) => { + return isMatrix(value1) ? value1[0][0]?.format : value1?.format; + }, + compute: function (...values) { + const maxa = reduceNumbersTextAs0(values, (acc, a) => { + return Math.max(a, acc); + }, -Infinity, this.locale); + return maxa === -Infinity ? 0 : maxa; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MAXIFS + // ----------------------------------------------------------------------------- + const MAXIFS = { + description: _t("Returns the maximum value in a range of cells, filtered by a set of criteria."), + args: [ + arg("range (range)", _t("The range of cells from which the maximum will be determined.")), + arg("criteria_range1 (range)", _t("The range of cells over which to evaluate criterion1.")), + arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.")), + arg("criteria_range2 (any, range, repeating)", _t("Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.")), + arg("criterion2 (string, repeating)", _t("The pattern or test to apply to criteria_range2.")), + ], + returns: ["NUMBER"], + compute: function (range, ...args) { + let result = -Infinity; + const _range = toMatrix(range); + visitMatchingRanges(args, (i, j) => { + const value = _range[i][j]; + if (typeof value === "number") { + result = result < value ? value : result; + } + }, this.locale); + return result === -Infinity ? 0 : result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MEDIAN + // ----------------------------------------------------------------------------- + const MEDIAN = { + description: _t("Median value in a numeric dataset."), + args: [ + arg("value1 (any, range)", _t("The first value or range to consider when calculating the median value.")), + arg("value2 (any, range, repeating)", _t("Additional values or ranges to consider when calculating the median value.")), + ], + returns: ["NUMBER"], + computeFormat: (value1) => { + return isMatrix(value1) ? value1[0][0]?.format : value1?.format; + }, + compute: function (...values) { + let data = []; + visitNumbers(values, (arg) => { + data.push(arg); + }, this.locale); + return centile(data, 0.5, true, this.locale); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MIN + // ----------------------------------------------------------------------------- + const MIN = { + description: _t("Minimum value in a numeric dataset."), + args: [ + arg("value1 (number, range)", _t("The first value or range to consider when calculating the minimum value.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to consider when calculating the minimum value.")), + ], + returns: ["NUMBER"], + computeFormat: (value1) => { + return isMatrix(value1) ? value1[0][0]?.format : value1?.format; + }, + compute: function (...values) { + const result = reduceNumbers(values, (acc, a) => (a < acc ? a : acc), Infinity, this.locale); + return result === Infinity ? 0 : result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MINA + // ----------------------------------------------------------------------------- + const MINA = { + description: _t("Minimum numeric value in a dataset."), + args: [ + arg("value1 (number, range)", _t("The first value or range to consider when calculating the minimum value.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to consider when calculating the minimum value.")), + ], + returns: ["NUMBER"], + computeFormat: (value1) => { + return isMatrix(value1) ? value1[0][0]?.format : value1?.format; + }, + compute: function (...values) { + const mina = reduceNumbersTextAs0(values, (acc, a) => { + return Math.min(a, acc); + }, Infinity, this.locale); + return mina === Infinity ? 0 : mina; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MINIFS + // ----------------------------------------------------------------------------- + const MINIFS = { + description: _t("Returns the minimum value in a range of cells, filtered by a set of criteria."), + args: [ + arg("range (range)", _t("The range of cells from which the minimum will be determined.")), + arg("criteria_range1 (range)", _t("The range of cells over which to evaluate criterion1.")), + arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.")), + arg("criteria_range2 (any, range, repeating)", _t("Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.")), + arg("criterion2 (string, repeating)", _t("The pattern or test to apply to criteria_range2.")), + ], + returns: ["NUMBER"], + compute: function (range, ...args) { + let result = Infinity; + const _range = toMatrix(range); + visitMatchingRanges(args, (i, j) => { + const value = _range[i][j]; + if (typeof value === "number") { + result = result > value ? value : result; + } + }, this.locale); + return result === Infinity ? 0 : result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // PEARSON + // ----------------------------------------------------------------------------- + const PEARSON = { + description: _t("Compute the Pearson product-moment correlation coefficient of a dataset."), + args: [ + arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), + arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), + ], + returns: ["NUMBER"], + compute: function (dataY, dataX) { + const { flatDataX, flatDataY } = filterAndFlatData(dataX, dataY); + if (flatDataX.length === 0) { + throw new Error(_t("[[FUNCTION_NAME]] expects non-empty ranges for both parameters.")); + } + if (flatDataX.length < 2) { + throw new Error(_t("[[FUNCTION_NAME]] needs at least two values for both parameters")); + } + const n = flatDataX.length; + let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0, sumYY = 0; + for (let i = 0; i < n; i++) { + const xij = flatDataX[i]; + const yij = flatDataY[i]; + sumX += xij; + sumY += yij; + sumXY += xij * yij; + sumXX += xij * xij; + sumYY += yij * yij; + } + return ((n * sumXY - sumX * sumY) / Math.sqrt((n * sumXX - sumX * sumX) * (n * sumYY - sumY * sumY))); + }, + isExported: true, + }; + // In GSheet, CORREL is just an alias to PEARSON + const CORREL = PEARSON; + // ----------------------------------------------------------------------------- + // PERCENTILE + // ----------------------------------------------------------------------------- + const PERCENTILE = { + description: _t("Value at a given percentile of a dataset."), + args: [ + arg("data (any, range)", _t("The array or range containing the dataset to consider.")), + arg("percentile (number)", _t("The percentile whose value within data will be calculated and returned.")), + ], + returns: ["NUMBER"], + computeFormat: (data) => { + return isMatrix(data) ? data[0][0]?.format : data?.format; + }, + compute: function (data, percentile) { + return PERCENTILE_INC.compute.bind(this)(data, percentile); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // PERCENTILE.EXC + // ----------------------------------------------------------------------------- + const PERCENTILE_EXC = { + description: _t("Value at a given percentile of a dataset exclusive of 0 and 1."), + args: [ + arg("data (any, range)", _t("The array or range containing the dataset to consider.")), + arg("percentile (number)", _t("The percentile, exclusive of 0 and 1, whose value within 'data' will be calculated and returned.")), + ], + returns: ["NUMBER"], + computeFormat: (data) => { + return isMatrix(data) ? data[0][0]?.format : data?.format; + }, + compute: function (data, percentile) { + return centile([data], percentile, false, this.locale); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // PERCENTILE.INC + // ----------------------------------------------------------------------------- + const PERCENTILE_INC = { + description: _t("Value at a given percentile of a dataset."), + args: [ + arg("data (any, range)", _t("The array or range containing the dataset to consider.")), + arg("percentile (number)", _t("The percentile whose value within data will be calculated and returned.")), + ], + returns: ["NUMBER"], + computeFormat: (data) => { + return isMatrix(data) ? data[0][0]?.format : data?.format; + }, + compute: function (data, percentile) { + return centile([data], percentile, true, this.locale); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // POLYFIT + // ----------------------------------------------------------------------------- + const POLYFIT_COEFFS = { + description: _t("Compute the coefficients of polynomial regression of the dataset."), + args: [ + arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), + arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), + arg("order (number)", _t("The order of the polynomial to fit the data, between 1 and 6.")), + arg("intercept (boolean, default=TRUE)", _t("A flag specifying whether to compute the intercept or not.")), + ], + returns: ["RANGE"], + compute: function (dataY, dataX, order, intercept = true) { + const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX); + return polynomialRegression(flatDataY, flatDataX, toNumber(order, this.locale), toBoolean(intercept)); + }, + isExported: false, + }; + // ----------------------------------------------------------------------------- + // POLYFIT.FORECAST + // ----------------------------------------------------------------------------- + const POLYFIT_FORECAST = { + description: _t("Predict value by computing a polynomial regression of the dataset."), + args: [ + arg("x (number, range)", _t("The value(s) on the x-axis to forecast.")), + arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), + arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), + arg("order (number)", _t("The order of the polynomial to fit the data, between 1 and 6.")), + arg("intercept (boolean, default=TRUE)", _t("A flag specifying whether to compute the intercept or not.")), + ], + returns: ["NUMBER"], + compute: function (x, dataY, dataX, order, intercept = true) { + const _order = toNumber(order, this.locale); + const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX); + const coeffs = polynomialRegression(flatDataY, flatDataX, _order, toBoolean(intercept)).flat(); + return matrixMap(toMatrix(x), (xij) => evaluatePolynomial(coeffs, toNumber(xij, this.locale), _order)); + }, + isExported: false, + }; + // ----------------------------------------------------------------------------- + // QUARTILE + // ----------------------------------------------------------------------------- + const QUARTILE = { + description: _t("Value nearest to a specific quartile of a dataset."), + args: [ + arg("data (any, range)", _t("The array or range containing the dataset to consider.")), + arg("quartile_number (number)", _t("Which quartile value to return.")), + ], + returns: ["NUMBER"], + computeFormat: (data) => { + return isMatrix(data) ? data[0][0]?.format : data?.format; + }, + compute: function (data, quartileNumber) { + return QUARTILE_INC.compute.bind(this)(data, quartileNumber); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // QUARTILE.EXC + // ----------------------------------------------------------------------------- + const QUARTILE_EXC = { + description: _t("Value nearest to a specific quartile of a dataset exclusive of 0 and 4."), + args: [ + arg("data (any, range)", _t("The array or range containing the dataset to consider.")), + arg("quartile_number (number)", _t("Which quartile value, exclusive of 0 and 4, to return.")), + ], + returns: ["NUMBER"], + computeFormat: (data) => { + return isMatrix(data) ? data[0][0]?.format : data?.format; + }, + compute: function (data, quartileNumber) { + const _quartileNumber = Math.trunc(toNumber(quartileNumber, this.locale)); + return centile([data], 0.25 * _quartileNumber, false, this.locale); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // QUARTILE.INC + // ----------------------------------------------------------------------------- + const QUARTILE_INC = { + description: _t("Value nearest to a specific quartile of a dataset."), + args: [ + arg("data (any, range)", _t("The array or range containing the dataset to consider.")), + arg("quartile_number (number)", _t("Which quartile value to return.")), + ], + returns: ["NUMBER"], + computeFormat: (data) => { + return isMatrix(data) ? data[0][0]?.format : data?.format; + }, + compute: function (data, quartileNumber) { + const _quartileNumber = Math.trunc(toNumber(quartileNumber, this.locale)); + return centile([data], 0.25 * _quartileNumber, true, this.locale); + }, + isExported: true, + }; + // RANK + // ----------------------------------------------------------------------------- + const RANK = { + description: _t("Returns the rank of a specified value in a dataset."), + args: [ + arg("value (number)", _t("The value whose rank will be determined.")), + arg("data (range)", _t("The range containing the dataset to consider.")), + arg("is_ascending (boolean, default=FALSE)", _t("Whether to consider the values in data in descending or ascending order.")), + ], + returns: ["ANY"], + compute: function (value, data, isAscending = false) { + const _isAscending = toBoolean(isAscending); + const _value = toNumber(value, this.locale); + let rank = 1; + let found = false; + for (const row of data) { + for (const cell of row) { + if (typeof cell !== "number") { + continue; + } + const _cell = toNumber(cell, this.locale); + if (_cell === _value) { + found = true; + } + else if (_cell > _value !== _isAscending) { + rank++; + } + } + } + if (!found) { + throw new NotAvailableError(_t("Value not found in the given data.")); + } + return rank; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // RSQ + // ----------------------------------------------------------------------------- + const RSQ = { + description: _t("Compute the square of r, the Pearson product-moment correlation coefficient of a dataset."), + args: [ + arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), + arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), + ], + returns: ["NUMBER"], + compute: function (dataY, dataX) { + const pearson = PEARSON.compute.bind(this); + return Math.pow(pearson(dataX, dataY), 2.0); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SLOPE + // ----------------------------------------------------------------------------- + const SLOPE = { + description: _t("Compute the slope of the linear regression."), + args: [ + arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), + arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), + ], + returns: ["NUMBER"], + compute: function (dataY, dataX) { + const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX); + const [[slope]] = fullLinearRegression([flatDataX], [flatDataY]); + return slope; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SMALL + // ----------------------------------------------------------------------------- + const SMALL = { + description: _t("Nth smallest element in a data set."), + args: [ + arg("data (any, range)", _t("The array or range containing the dataset to consider.")), + arg("n (number)", _t("The rank from smallest to largest of the element to return.")), + ], + returns: ["NUMBER"], + computeValueAndFormat: function (data, n) { + const _n = Math.trunc(toNumber(n?.value, this.locale)); + let largests = []; + let index; + let count = 0; + visitAny([data], (d) => { + if (typeof d.value === "number") { + index = dichotomicSearch(largests, d.value, "nextSmaller", "asc", largests.length, (array, i) => array[i].value); + largests.splice(index + 1, 0, d); + count++; + if (count > _n) { + largests.pop(); + count--; + } + } + }); + const result = largests.pop(); + assert(() => result !== undefined, _t("[[FUNCTION_NAME]] has no valid input data.")); + assert(() => count >= _n, _t("Function [[FUNCTION_NAME]] parameter 2 value (%s) is out of range.", _n)); + return result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SPEARMAN + // ----------------------------------------------------------------------------- + const SPEARMAN = { + description: _t("Compute the Spearman rank correlation coefficient of a dataset."), + args: [ + arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), + arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), + ], + returns: ["NUMBER"], + compute: function (dataX, dataY) { + const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX); + const n = flatDataX.length; + const order = flatDataX.map((e, i) => [e, flatDataY[i]]); + order.sort((a, b) => a[0] - b[0]); + for (let i = 0; i < n; ++i) { + order[i][0] = i; + } + order.sort((a, b) => a[1] - b[1]); + let sum = 0.0; + for (let i = 0; i < n; ++i) { + sum += (order[i][0] - i) ** 2; + } + return 1 - (6 * sum) / (n ** 3 - n); + }, + isExported: false, + }; + // ----------------------------------------------------------------------------- + // STDEV + // ----------------------------------------------------------------------------- + const STDEV = { + description: _t("Standard deviation."), + args: [ + arg("value1 (number, range)", _t("The first value or range of the sample.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the sample.")), + ], + returns: ["NUMBER"], + compute: function (...values) { + return Math.sqrt(VAR.compute.bind(this)(...values)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // STDEV.P + // ----------------------------------------------------------------------------- + const STDEV_P = { + description: _t("Standard deviation of entire population."), + args: [ + arg("value1 (number, range)", _t("The first value or range of the population.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the population.")), + ], + returns: ["NUMBER"], + compute: function (...values) { + return Math.sqrt(VAR_P.compute.bind(this)(...values)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // STDEV.S + // ----------------------------------------------------------------------------- + const STDEV_S = { + description: _t("Standard deviation."), + args: [ + arg("value1 (number, range)", _t("The first value or range of the sample.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the sample.")), + ], + returns: ["NUMBER"], + compute: function (...values) { + return Math.sqrt(VAR_S.compute.bind(this)(...values)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // STDEVA + // ----------------------------------------------------------------------------- + const STDEVA = { + description: _t("Standard deviation of sample (text as 0)."), + args: [ + arg("value1 (number, range)", _t("The first value or range of the sample.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the sample.")), + ], + returns: ["NUMBER"], + compute: function (...values) { + return Math.sqrt(VARA.compute.bind(this)(...values)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // STDEVP + // ----------------------------------------------------------------------------- + const STDEVP = { + description: _t("Standard deviation of entire population."), + args: [ + arg("value1 (number, range)", _t("The first value or range of the population.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the population.")), + ], + returns: ["NUMBER"], + compute: function (...values) { + return Math.sqrt(VARP.compute.bind(this)(...values)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // STDEVPA + // ----------------------------------------------------------------------------- + const STDEVPA = { + description: _t("Standard deviation of entire population (text as 0)."), + args: [ + arg("value1 (number, range)", _t("The first value or range of the population.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the population.")), + ], + returns: ["NUMBER"], + compute: function (...values) { + return Math.sqrt(VARPA.compute.bind(this)(...values)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // STEYX + // ----------------------------------------------------------------------------- + const STEYX = { + description: _t("Calculates the standard error of the predicted y-value for each x in the regression of a dataset."), + args: [ + arg("data_y (range)", _t("The range representing the array or matrix of dependent data.")), + arg("data_x (range)", _t("The range representing the array or matrix of independent data.")), + ], + returns: ["NUMBER"], + compute: function (dataY, dataX) { + const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX); + const data = fullLinearRegression([flatDataX], [flatDataY], true, true); + return data[1][2]; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TREND + // ----------------------------------------------------------------------------- + const TREND = { + description: _t("Fits points to linear trend derived via least-squares."), + args: [ + arg("known_data_y (number, range)", _t("The array or range containing dependent (y) values that are already known, used to curve fit an ideal linear trend.")), + arg("known_data_x (number, range, optional, default={1;2;3;...})", _t("The values of the independent variable(s) corresponding with known_data_y.")), + arg("new_data_x (number, range, optional, default=known_data_x)", _t("The data points to return the y values for on the ideal curve fit.")), + arg("b (boolean, optional, default=TRUE)", _t("Given a general linear form of y = m*x+b for a curve fit, calculates b if TRUE or forces b to be 0 and only calculates the m values if FALSE, i.e. forces the curve fit to pass through the origin.")), + ], + returns: ["NUMBER"], + compute: function (knownDataY, knownDataX = [], newDataX = [], b = true) { + return predictLinearValues(tryCastAsNumberMatrix(knownDataY, "the first argument (known_data_y)"), tryCastAsNumberMatrix(knownDataX, "the second argument (known_data_x)"), tryCastAsNumberMatrix(newDataX, "the third argument (new_data_y)"), toBoolean(b)); + }, + }; + // ----------------------------------------------------------------------------- + // VAR + // ----------------------------------------------------------------------------- + const VAR = { + description: _t("Variance."), + args: [ + arg("value1 (number, range)", _t("The first value or range of the sample.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the sample.")), + ], + returns: ["NUMBER"], + compute: function (...values) { + return variance(values, true, false, this.locale); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // VAR.P + // ----------------------------------------------------------------------------- + const VAR_P = { + description: _t("Variance of entire population."), + args: [ + arg("value1 (number, range)", _t("The first value or range of the population.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the population.")), + ], + returns: ["NUMBER"], + compute: function (...values) { + return variance(values, false, false, this.locale); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // VAR.S + // ----------------------------------------------------------------------------- + const VAR_S = { + description: _t("Variance."), + args: [ + arg("value1 (number, range)", _t("The first value or range of the sample.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the sample.")), + ], + returns: ["NUMBER"], + compute: function (...values) { + return variance(values, true, false, this.locale); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // VARA + // ----------------------------------------------------------------------------- + const VARA = { + description: _t("Variance of sample (text as 0)."), + args: [ + arg("value1 (number, range)", _t("The first value or range of the sample.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the sample.")), + ], + returns: ["NUMBER"], + compute: function (...values) { + return variance(values, true, true, this.locale); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // VARP + // ----------------------------------------------------------------------------- + const VARP = { + description: _t("Variance of entire population."), + args: [ + arg("value1 (number, range)", _t("The first value or range of the population.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the population.")), + ], + returns: ["NUMBER"], + compute: function (...values) { + return variance(values, false, false, this.locale); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // VARPA + // ----------------------------------------------------------------------------- + const VARPA = { + description: _t("Variance of entire population (text as 0)."), + args: [ + arg("value1 (number, range)", _t("The first value or range of the population.")), + arg("value2 (number, range, repeating)", _t("Additional values or ranges to include in the population.")), + ], + returns: ["NUMBER"], + compute: function (...values) { + return variance(values, false, true, this.locale); + }, + isExported: true, + }; + + var statistical = /*#__PURE__*/Object.freeze({ + __proto__: null, + AVEDEV: AVEDEV, + AVERAGE: AVERAGE, + AVERAGEA: AVERAGEA, + AVERAGEIF: AVERAGEIF, + AVERAGEIFS: AVERAGEIFS, + AVERAGE_WEIGHTED: AVERAGE_WEIGHTED, + CORREL: CORREL, + COUNT: COUNT, + COUNTA: COUNTA, + COVAR: COVAR, + COVARIANCE_P: COVARIANCE_P, + COVARIANCE_S: COVARIANCE_S, + FORECAST: FORECAST, + GROWTH: GROWTH, + INTERCEPT: INTERCEPT, + LARGE: LARGE, + LINEST: LINEST, + LOGEST: LOGEST, + MATTHEWS: MATTHEWS, + MAX: MAX, + MAXA: MAXA, + MAXIFS: MAXIFS, + MEDIAN: MEDIAN, + MIN: MIN, + MINA: MINA, + MINIFS: MINIFS, + PEARSON: PEARSON, + PERCENTILE: PERCENTILE, + PERCENTILE_EXC: PERCENTILE_EXC, + PERCENTILE_INC: PERCENTILE_INC, + POLYFIT_COEFFS: POLYFIT_COEFFS, + POLYFIT_FORECAST: POLYFIT_FORECAST, + QUARTILE: QUARTILE, + QUARTILE_EXC: QUARTILE_EXC, + QUARTILE_INC: QUARTILE_INC, + RANK: RANK, + RSQ: RSQ, + SLOPE: SLOPE, + SMALL: SMALL, + SPEARMAN: SPEARMAN, + STDEV: STDEV, + STDEVA: STDEVA, + STDEVP: STDEVP, + STDEVPA: STDEVPA, + STDEV_P: STDEV_P, + STDEV_S: STDEV_S, + STEYX: STEYX, + TREND: TREND, + VAR: VAR, + VARA: VARA, + VARP: VARP, + VARPA: VARPA, + VAR_P: VAR_P, + VAR_S: VAR_S + }); + + function getMatchingCells(database, field, criteria, locale) { + // Example + // # DATABASE # CRITERIA # field = "C" + // + // | A | B | C | | A | C | + // |===========| |=======| + // | 1 | x | j | |<2 | j | + // | 1 | Z | k | | | 7 | + // | 5 | y | 7 | + // 1 - Select coordinates of database columns ---------------------------------------------------- + const indexColNameDB = new Map(); + const dimRowDB = database.length; + for (let indexCol = dimRowDB - 1; indexCol >= 0; indexCol--) { + indexColNameDB.set(toString(database[indexCol][0]).toUpperCase(), indexCol); + } + // Example continuation: indexColNameDB = {"A" => 0, "B" => 1, "C" => 2} + // 2 - Check if the field parameter exists in the column names of the database ------------------- + // field may either be a text label corresponding to a column header in the + // first row of database or a numeric index indicating which column to consider, + // where the first column has the value 1. + if (typeof field !== "number" && typeof field !== "string") { + throw new Error(_t("The field must be a number or a string")); + } + let index; + if (typeof field === "number") { + index = Math.trunc(field) - 1; + if (index < 0 || dimRowDB - 1 < index) { + throw new Error(_t("The field (%s) must be one of %s or must be a number between 1 and %s inclusive.", field.toString(), dimRowDB.toString())); + } + } + else { + const colName = toString(field).toUpperCase(); + index = indexColNameDB.get(colName) ?? -1; + if (index === -1) { + throw new Error(_t("The field (%s) must be one of %s.", toString(field), [...indexColNameDB.keys()].toString())); + } + } + // Example continuation: index = 2 + // 3 - For each criteria row, find database row that correspond ---------------------------------- + const dimColCriteria = criteria[0].length; + if (dimColCriteria < 2) { + throw new Error(_t("The criteria range contains %s row, it must be at least 2 rows.", dimColCriteria.toString())); + } + let matchingRows = new Set(); + const dimColDB = database[0].length; + for (let indexRow = 1; indexRow < dimColCriteria; indexRow++) { + let args = []; + let existColNameDB = true; + for (let indexCol = 0; indexCol < criteria.length; indexCol++) { + const currentName = toString(criteria[indexCol][0]).toUpperCase(); + const indexColDB = indexColNameDB.get(currentName); + const criter = criteria[indexCol][indexRow]; + if (criter !== null) { + if (indexColDB !== undefined) { + args.push([database[indexColDB].slice(1, dimColDB)]); + args.push(criter); + } + else { + existColNameDB = false; + break; + } + } + } + // Example continuation: args1 = [[1,1,5], "<2", ["j","k",7], "j"] + // Example continuation: args2 = [["j","k",7], "7"] + if (existColNameDB) { + if (args.length > 0) { + visitMatchingRanges(args, (i, j) => { + matchingRows.add(j); + }, locale, true); + } + else { + // return indices of each database row when a criteria table row is void + matchingRows = new Set(Array(dimColDB - 1).keys()); + break; + } + } + } + // Example continuation: matchingRows = {0, 2} + // 4 - return for each database row corresponding, the cells corresponding to the field parameter + const fieldCol = database[index]; + // Example continuation:: fieldCol = ["C", "j", "k", 7] + const matchingCells = [...matchingRows].map((x) => fieldCol[x + 1]); + // Example continuation:: matchingCells = ["j", 7] + return matchingCells; + } + const databaseArgs = [ + arg("database (range)", _t("The array or range containing the data to consider, structured in such a way that the first row contains the labels for each column's values.")), + arg("field (any)", _t("Indicates which column in database contains the values to be extracted and operated on.")), + arg("criteria (range)", _t("An array or range containing zero or more criteria to filter the database values by before operating.")), + ]; + // ----------------------------------------------------------------------------- + // DAVERAGE + // ----------------------------------------------------------------------------- + const DAVERAGE = { + description: _t("Average of a set of values from a table-like range."), + args: databaseArgs, + returns: ["NUMBER"], + compute: function (database, field, criteria) { + const cells = getMatchingCells(database, field, criteria, this.locale); + return AVERAGE.compute.bind(this)([cells]); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DCOUNT + // ----------------------------------------------------------------------------- + const DCOUNT = { + description: _t("Counts values from a table-like range."), + args: databaseArgs, + returns: ["NUMBER"], + compute: function (database, field, criteria) { + const cells = getMatchingCells(database, field, criteria, this.locale); + return COUNT.compute.bind(this)([cells]); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DCOUNTA + // ----------------------------------------------------------------------------- + const DCOUNTA = { + description: _t("Counts values and text from a table-like range."), + args: databaseArgs, + returns: ["NUMBER"], + compute: function (database, field, criteria) { + const cells = getMatchingCells(database, field, criteria, this.locale); + return COUNTA.compute.bind(this)([cells]); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DGET + // ----------------------------------------------------------------------------- + const DGET = { + description: _t("Single value from a table-like range."), + args: databaseArgs, + returns: ["NUMBER"], + compute: function (database, field, criteria) { + const cells = getMatchingCells(database, field, criteria, this.locale); + assert(() => cells.length === 1, _t("More than one match found in DGET evaluation.")); + return cells[0]; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DMAX + // ----------------------------------------------------------------------------- + const DMAX = { + description: _t("Maximum of values from a table-like range."), + args: databaseArgs, + returns: ["NUMBER"], + compute: function (database, field, criteria) { + const cells = getMatchingCells(database, field, criteria, this.locale); + return MAX.compute.bind(this)([cells]); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DMIN + // ----------------------------------------------------------------------------- + const DMIN = { + description: _t("Minimum of values from a table-like range."), + args: databaseArgs, + returns: ["NUMBER"], + compute: function (database, field, criteria) { + const cells = getMatchingCells(database, field, criteria, this.locale); + return MIN.compute.bind(this)([cells]); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DPRODUCT + // ----------------------------------------------------------------------------- + const DPRODUCT = { + description: _t("Product of values from a table-like range."), + args: databaseArgs, + returns: ["NUMBER"], + compute: function (database, field, criteria) { + const cells = getMatchingCells(database, field, criteria, this.locale); + return PRODUCT.compute.bind(this)([cells]); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DSTDEV + // ----------------------------------------------------------------------------- + const DSTDEV = { + description: _t("Standard deviation of population sample from table."), + args: databaseArgs, + returns: ["NUMBER"], + compute: function (database, field, criteria) { + const cells = getMatchingCells(database, field, criteria, this.locale); + return STDEV.compute.bind(this)([cells]); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DSTDEVP + // ----------------------------------------------------------------------------- + const DSTDEVP = { + description: _t("Standard deviation of entire population from table."), + args: databaseArgs, + returns: ["NUMBER"], + compute: function (database, field, criteria) { + const cells = getMatchingCells(database, field, criteria, this.locale); + return STDEVP.compute.bind(this)([cells]); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DSUM + // ----------------------------------------------------------------------------- + const DSUM = { + description: _t("Sum of values from a table-like range."), + args: databaseArgs, + returns: ["NUMBER"], + compute: function (database, field, criteria) { + const cells = getMatchingCells(database, field, criteria, this.locale); + return SUM.compute.bind(this)([cells]); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DVAR + // ----------------------------------------------------------------------------- + const DVAR = { + description: _t("Variance of population sample from table-like range."), + args: databaseArgs, + returns: ["NUMBER"], + compute: function (database, field, criteria) { + const cells = getMatchingCells(database, field, criteria, this.locale); + return VAR.compute.bind(this)([cells]); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DVARP + // ----------------------------------------------------------------------------- + const DVARP = { + description: _t("Variance of a population from a table-like range."), + args: databaseArgs, + returns: ["NUMBER"], + compute: function (database, field, criteria) { + const cells = getMatchingCells(database, field, criteria, this.locale); + return VARP.compute.bind(this)([cells]); + }, + isExported: true, + }; + + var database = /*#__PURE__*/Object.freeze({ + __proto__: null, + DAVERAGE: DAVERAGE, + DCOUNT: DCOUNT, + DCOUNTA: DCOUNTA, + DGET: DGET, + DMAX: DMAX, + DMIN: DMIN, + DPRODUCT: DPRODUCT, + DSTDEV: DSTDEV, + DSTDEVP: DSTDEVP, + DSUM: DSUM, + DVAR: DVAR, + DVARP: DVARP + }); + + const DEFAULT_TYPE = 1; + const DEFAULT_WEEKEND = 1; + var TIME_UNIT; + (function (TIME_UNIT) { + TIME_UNIT["WHOLE_YEARS"] = "Y"; + TIME_UNIT["WHOLE_MONTHS"] = "M"; + TIME_UNIT["WHOLE_DAYS"] = "D"; + TIME_UNIT["DAYS_WITHOUT_WHOLE_MONTHS"] = "MD"; + TIME_UNIT["MONTH_WITHOUT_WHOLE_YEARS"] = "YM"; + TIME_UNIT["DAYS_BETWEEN_NO_MORE_THAN_ONE_YEAR"] = "YD"; + })(TIME_UNIT || (TIME_UNIT = {})); + // ----------------------------------------------------------------------------- + // DATE + // ----------------------------------------------------------------------------- + const DATE = { + description: _t("Converts year/month/day into a date."), + args: [ + arg("year (number)", _t("The year component of the date.")), + arg("month (number)", _t("The month component of the date.")), + arg("day (number)", _t("The day component of the date.")), + ], + returns: ["DATE"], + computeFormat: function () { + return this.locale.dateFormat; + }, + compute: function (year, month, day) { + let _year = Math.trunc(toNumber(year, this.locale)); + const _month = Math.trunc(toNumber(month, this.locale)); + const _day = Math.trunc(toNumber(day, this.locale)); + // For years less than 0 or greater than 10000, return #ERROR. + assert(() => 0 <= _year && _year <= 9999, _t("The year (%s) must be between 0 and 9999 inclusive.", _year.toString())); + // Between 0 and 1899, we add that value to 1900 to calculate the year + if (_year < 1900) { + _year += 1900; + } + const jsDate = new Date(_year, _month - 1, _day); + const result = jsDateToRoundNumber(jsDate); + assert(() => result >= 0, _t("The function [[FUNCTION_NAME]] result must be greater than or equal 01/01/1900.")); + return result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DATEDIF + // ----------------------------------------------------------------------------- + const DATEDIF = { + description: _t("Calculates the number of days, months, or years between two dates."), + args: [ + arg("start_date (date)", _t("The start date to consider in the calculation. Must be a reference to a cell containing a DATE, a function returning a DATE type, or a number.")), + arg("end_date (date)", _t("The end date to consider in the calculation. Must be a reference to a cell containing a DATE, a function returning a DATE type, or a number.")), + arg("unit (string)", _t('A text abbreviation for unit of time. Accepted values are "Y" (the number of whole years between start_date and end_date), "M" (the number of whole months between start_date and end_date), "D" (the number of days between start_date and end_date), "MD" (the number of days between start_date and end_date after subtracting whole months), "YM" (the number of whole months between start_date and end_date after subtracting whole years), "YD" (the number of days between start_date and end_date, assuming start_date and end_date were no more than one year apart).')), + ], + returns: ["NUMBER"], + compute: function (startDate, endDate, unit) { + const _unit = toString(unit).toUpperCase(); + assert(() => Object.values(TIME_UNIT).includes(_unit), expectStringSetError(Object.values(TIME_UNIT), toString(unit))); + const _startDate = Math.trunc(toNumber(startDate, this.locale)); + const _endDate = Math.trunc(toNumber(endDate, this.locale)); + const jsStartDate = numberToJsDate(_startDate); + const jsEndDate = numberToJsDate(_endDate); + assert(() => _endDate >= _startDate, _t("start_date (%s) should be on or before end_date (%s).", jsStartDate.toLocaleDateString(), jsEndDate.toLocaleDateString())); + switch (_unit) { + case TIME_UNIT.WHOLE_YEARS: + return getTimeDifferenceInWholeYears(jsStartDate, jsEndDate); + case TIME_UNIT.WHOLE_MONTHS: + return getTimeDifferenceInWholeMonths(jsStartDate, jsEndDate); + case TIME_UNIT.WHOLE_DAYS: { + return getTimeDifferenceInWholeDays(jsStartDate, jsEndDate); + } + case TIME_UNIT.MONTH_WITHOUT_WHOLE_YEARS: { + return (getTimeDifferenceInWholeMonths(jsStartDate, jsEndDate) - + getTimeDifferenceInWholeYears(jsStartDate, jsEndDate) * 12); + } + case TIME_UNIT.DAYS_WITHOUT_WHOLE_MONTHS: + // Using "MD" may get incorrect result in Excel + // See: https://support.microsoft.com/en-us/office/datedif-function-25dba1a4-2812-480b-84dd-8b32a451b35c + let days = jsEndDate.getDate() - jsStartDate.getDate(); + if (days < 0) { + const monthBeforeEndMonth = new Date(jsEndDate.getFullYear(), jsEndDate.getMonth() - 1, 1); + const daysInMonthBeforeEndMonth = getDaysInMonth(monthBeforeEndMonth); + days = daysInMonthBeforeEndMonth - Math.abs(days); + } + return days; + case TIME_UNIT.DAYS_BETWEEN_NO_MORE_THAN_ONE_YEAR: { + if (areTwoDatesWithinOneYear(_startDate, _endDate)) { + return getTimeDifferenceInWholeDays(jsStartDate, jsEndDate); + } + const endDateWithinOneYear = new Date(jsStartDate.getFullYear(), jsEndDate.getMonth(), jsEndDate.getDate()); + let days = getTimeDifferenceInWholeDays(jsStartDate, endDateWithinOneYear); + if (days < 0) { + endDateWithinOneYear.setFullYear(jsStartDate.getFullYear() + 1); + days = getTimeDifferenceInWholeDays(jsStartDate, endDateWithinOneYear); + } + return days; + } + } + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DATEVALUE + // ----------------------------------------------------------------------------- + const DATEVALUE = { + description: _t("Converts a date string to a date value."), + args: [arg("date_string (string)", _t("The string representing the date."))], + returns: ["NUMBER"], + compute: function (dateString) { + const _dateString = toString(dateString); + const internalDate = parseDateTime(_dateString, this.locale); + assert(() => internalDate !== null, _t("The date_string (%s) cannot be parsed to date/time.", _dateString.toString())); + return Math.trunc(internalDate.value); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DAY + // ----------------------------------------------------------------------------- + const DAY = { + description: _t("Day of the month that a specific date falls on."), + args: [arg("date (string)", _t("The date from which to extract the day."))], + returns: ["NUMBER"], + compute: function (date) { + return toJsDate(date, this.locale).getDate(); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DAYS + // ----------------------------------------------------------------------------- + const DAYS = { + description: _t("Number of days between two dates."), + args: [ + arg("end_date (date)", _t("The end of the date range.")), + arg("start_date (date)", _t("The start of the date range.")), + ], + returns: ["NUMBER"], + compute: function (endDate, startDate) { + const _endDate = toJsDate(endDate, this.locale); + const _startDate = toJsDate(startDate, this.locale); + const dateDif = _endDate.getTime() - _startDate.getTime(); + return Math.round(dateDif / MS_PER_DAY); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DAYS360 + // ----------------------------------------------------------------------------- + const DEFAULT_DAY_COUNT_METHOD = 0; + const DAYS360 = { + description: _t("Number of days between two dates on a 360-day year (months of 30 days)."), + args: [ + arg("start_date (date)", _t("The start date to consider in the calculation.")), + arg("end_date (date)", _t("The end date to consider in the calculation.")), + arg(`method (number, default=${DEFAULT_DAY_COUNT_METHOD})`, _t("An indicator of what day count method to use. (0) US NASD method (1) European method")), + ], + returns: ["NUMBER"], + compute: function (startDate, endDate, method = DEFAULT_DAY_COUNT_METHOD) { + const _startDate = toNumber(startDate, this.locale); + const _endDate = toNumber(endDate, this.locale); + const dayCountConvention = toBoolean(method) ? 4 : 0; + const yearFrac = YEARFRAC.compute.bind(this)(startDate, endDate, dayCountConvention); + return Math.sign(_endDate - _startDate) * Math.round(yearFrac * 360); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // EDATE + // ----------------------------------------------------------------------------- + const EDATE = { + description: _t("Date a number of months before/after another date."), + args: [ + arg("start_date (date)", _t("The date from which to calculate the result.")), + arg("months (number)", _t("The number of months before (negative) or after (positive) 'start_date' to calculate.")), + ], + returns: ["DATE"], + computeFormat: function () { + return this.locale.dateFormat; + }, + compute: function (startDate, months) { + const _startDate = toJsDate(startDate, this.locale); + const _months = Math.trunc(toNumber(months, this.locale)); + const jsDate = addMonthsToDate(_startDate, _months, false); + return jsDateToRoundNumber(jsDate); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // EOMONTH + // ----------------------------------------------------------------------------- + const EOMONTH = { + description: _t("Last day of a month before or after a date."), + args: [ + arg("start_date (date)", _t("The date from which to calculate the result.")), + arg("months (number)", _t("The number of months before (negative) or after (positive) 'start_date' to consider.")), + ], + returns: ["DATE"], + computeFormat: function () { + return this.locale.dateFormat; + }, + compute: function (startDate, months) { + const _startDate = toJsDate(startDate, this.locale); + const _months = Math.trunc(toNumber(months, this.locale)); + const yStart = _startDate.getFullYear(); + const mStart = _startDate.getMonth(); + const jsDate = new Date(yStart, mStart + _months + 1, 0); + return jsDateToRoundNumber(jsDate); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // HOUR + // ----------------------------------------------------------------------------- + const HOUR = { + description: _t("Hour component of a specific time."), + args: [arg("time (date)", _t("The time from which to calculate the hour component."))], + returns: ["NUMBER"], + compute: function (date) { + return toJsDate(date, this.locale).getHours(); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ISOWEEKNUM + // ----------------------------------------------------------------------------- + const ISOWEEKNUM = { + description: _t("ISO week number of the year."), + args: [ + arg("date (date)", _t("The date for which to determine the ISO week number. Must be a reference to a cell containing a date, a function returning a date type, or a number.")), + ], + returns: ["NUMBER"], + compute: function (date) { + const _date = toJsDate(date, this.locale); + const y = _date.getFullYear(); + // 1 - As the 1st week of a year can start the previous year or after the 1st + // january we first look if the date is in the weeks of the current year, previous + // year or year after. + // A - We look for the current year, the first days of the first week + // and the last days of the last week + // The first week of the year is the week that contains the first + // Thursday of the year. + let firstThursday = 1; + while (new Date(y, 0, firstThursday).getDay() !== 4) { + firstThursday += 1; + } + const firstDayOfFirstWeek = new Date(y, 0, firstThursday - 3); + // The last week of the year is the week that contains the last Thursday of + // the year. + let lastThursday = 31; + while (new Date(y, 11, lastThursday).getDay() !== 4) { + lastThursday -= 1; + } + const lastDayOfLastWeek = new Date(y, 11, lastThursday + 3); + // B - If our date > lastDayOfLastWeek then it's in the weeks of the year after + // If our date < firstDayOfFirstWeek then it's in the weeks of the year before + let offsetYear; + if (firstDayOfFirstWeek.getTime() <= _date.getTime()) { + if (_date.getTime() <= lastDayOfLastWeek.getTime()) { + offsetYear = 0; + } + else { + offsetYear = 1; + } + } + else { + offsetYear = -1; + } + // 2 - now that the year is known, we are looking at the difference between + // the first day of this year and the date. The difference in days divided by + // 7 gives us the week number + let firstDay; + switch (offsetYear) { + case 0: + firstDay = firstDayOfFirstWeek; + break; + case 1: + // firstDay is the 1st day of the 1st week of the year after + // firstDay = lastDayOfLastWeek + 1 Day + firstDay = new Date(y, 11, lastThursday + 3 + 1); + break; + case -1: + // firstDay is the 1st day of the 1st week of the previous year. + // The first week of the previous year is the week that contains the + // first Thursday of the previous year. + let firstThursdayPreviousYear = 1; + while (new Date(y - 1, 0, firstThursdayPreviousYear).getDay() !== 4) { + firstThursdayPreviousYear += 1; + } + firstDay = new Date(y - 1, 0, firstThursdayPreviousYear - 3); + break; + } + const diff = (_date.getTime() - firstDay.getTime()) / MS_PER_DAY; + return Math.floor(diff / 7) + 1; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MINUTE + // ----------------------------------------------------------------------------- + const MINUTE = { + description: _t("Minute component of a specific time."), + args: [arg("time (date)", _t("The time from which to calculate the minute component."))], + returns: ["NUMBER"], + compute: function (date) { + return toJsDate(date, this.locale).getMinutes(); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MONTH + // ----------------------------------------------------------------------------- + const MONTH = { + description: _t("Month of the year a specific date falls in"), + args: [arg("date (date)", _t("The date from which to extract the month."))], + returns: ["NUMBER"], + compute: function (date) { + return toJsDate(date, this.locale).getMonth() + 1; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // NETWORKDAYS + // ----------------------------------------------------------------------------- + const NETWORKDAYS = { + description: _t("Net working days between two provided days."), + args: [ + arg("start_date (date)", _t("The start date of the period from which to calculate the number of net working days.")), + arg("end_date (date)", _t("The end date of the period from which to calculate the number of net working days.")), + arg("holidays (date, range, optional)", _t("A range or array constant containing the date serial numbers to consider holidays.")), + ], + returns: ["NUMBER"], + compute: function (startDate, endDate, holidays) { + return NETWORKDAYS_INTL.compute.bind(this)(startDate, endDate, 1, holidays); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // NETWORKDAYS.INTL + // ----------------------------------------------------------------------------- + /** + * Transform weekend Spreadsheet information into Date Day JavaScript information. + * Take string (String method) or number (Number method), return array of numbers. + * + * String method: weekends can be specified using seven 0’s and 1’s, where the + * first number in the set represents Monday and the last number is for Sunday. + * A zero means that the day is a work day, a 1 means that the day is a weekend. + * For example, “0000011” would mean Saturday and Sunday are weekends. + * + * Number method: instead of using the string method above, a single number can + * be used. 1 = Saturday/Sunday are weekends, 2 = Sunday/Monday, and this pattern + * repeats until 7 = Friday/Saturday. 11 = Sunday is the only weekend, 12 = Monday + * is the only weekend, and this pattern repeats until 17 = Saturday is the only + * weekend. + * + * Example: + * - 11 return [0] (correspond to Sunday) + * - 12 return [1] (correspond to Monday) + * - 3 return [1,2] (correspond to Monday and Tuesday) + * - "0101010" return [2,4,6] (correspond to Tuesday, Thursday and Saturday) + */ + function weekendToDayNumber(weekend) { + // case "string" + if (typeof weekend === "string") { + assert(() => { + if (weekend.length !== 7) { + return false; + } + for (let day of weekend) { + if (day !== "0" && day !== "1") { + return false; + } + } + return true; + }, _t('When weekend is a string (%s) it must be composed of "0" or "1".', weekend)); + let result = []; + for (let i = 0; i < 7; i++) { + if (weekend[i] === "1") { + result.push((i + 1) % 7); + } + } + return result; + } + //case "number" + if (typeof weekend === "number") { + assert(() => (1 <= weekend && weekend <= 7) || (11 <= weekend && weekend <= 17), _t("The weekend (%s) must be a string or a number in the range 1-7 or 11-17.", weekend.toString())); + // case 1 <= weekend <= 7 + if (weekend <= 7) { + // 1 = Saturday/Sunday are weekends + // 2 = Sunday/Monday + // ... + // 7 = Friday/Saturday. + return [weekend - 2 === -1 ? 6 : weekend - 2, weekend - 1]; + } + // case 11 <= weekend <= 17 + // 11 = Sunday is the only weekend + // 12 = Monday is the only weekend + // ... + // 17 = Saturday is the only weekend. + return [weekend - 11]; + } + throw Error(_t("The weekend must be a number or a string.")); + } + const NETWORKDAYS_INTL = { + description: _t("Net working days between two dates (specifying weekends)."), + args: [ + arg("start_date (date)", _t("The start date of the period from which to calculate the number of net working days.")), + arg("end_date (date)", _t("The end date of the period from which to calculate the number of net working days.")), + arg(`weekend (any, default=${DEFAULT_WEEKEND})`, _t("A number or string representing which days of the week are considered weekends.")), + arg("holidays (date, range, optional)", _t("A range or array constant containing the dates to consider as holidays.")), + ], + returns: ["NUMBER"], + compute: function (startDate, endDate, weekend = DEFAULT_WEEKEND, holidays) { + const _startDate = toJsDate(startDate, this.locale); + const _endDate = toJsDate(endDate, this.locale); + const daysWeekend = weekendToDayNumber(weekend); + let timesHoliday = new Set(); + if (holidays !== undefined) { + visitAny([holidays], (h) => { + const holiday = toJsDate(h, this.locale); + timesHoliday.add(holiday.getTime()); + }); + } + const invertDate = _startDate.getTime() > _endDate.getTime(); + const stopDate = new Date((invertDate ? _startDate : _endDate).getTime()); + let stepDate = new Date((invertDate ? _endDate : _startDate).getTime()); + const timeStopDate = stopDate.getTime(); + let timeStepDate = stepDate.getTime(); + let netWorkingDay = 0; + while (timeStepDate <= timeStopDate) { + if (!daysWeekend.includes(stepDate.getDay()) && !timesHoliday.has(timeStepDate)) { + netWorkingDay += 1; + } + stepDate.setDate(stepDate.getDate() + 1); + timeStepDate = stepDate.getTime(); + } + return invertDate ? -netWorkingDay : netWorkingDay; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // NOW + // ----------------------------------------------------------------------------- + const NOW = { + description: _t("Current date and time as a date value."), + args: [], + returns: ["DATE"], + computeFormat: function () { + return getDateTimeFormat(this.locale); + }, + compute: function () { + let today = new Date(); + today.setMilliseconds(0); + const delta = today.getTime() - INITIAL_1900_DAY.getTime(); + const time = today.getHours() / 24 + today.getMinutes() / 1440 + today.getSeconds() / 86400; + return Math.floor(delta / MS_PER_DAY) + time; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SECOND + // ----------------------------------------------------------------------------- + const SECOND = { + description: _t("Minute component of a specific time."), + args: [arg("time (date)", _t("The time from which to calculate the second component."))], + returns: ["NUMBER"], + compute: function (date) { + return toJsDate(date, this.locale).getSeconds(); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TIME + // ----------------------------------------------------------------------------- + const TIME = { + description: _t("Converts hour/minute/second into a time."), + args: [ + arg("hour (number)", _t("The hour component of the time.")), + arg("minute (number)", _t("The minute component of the time.")), + arg("second (number)", _t("The second component of the time.")), + ], + returns: ["DATE"], + computeFormat: function () { + return this.locale.timeFormat; + }, + compute: function (hour, minute, second) { + let _hour = Math.trunc(toNumber(hour, this.locale)); + let _minute = Math.trunc(toNumber(minute, this.locale)); + let _second = Math.trunc(toNumber(second, this.locale)); + _minute += Math.floor(_second / 60); + _second = (_second % 60) + (_second < 0 ? 60 : 0); + _hour += Math.floor(_minute / 60); + _minute = (_minute % 60) + (_minute < 0 ? 60 : 0); + _hour %= 24; + assert(() => _hour >= 0, _t("The function [[FUNCTION_NAME]] result cannot be negative")); + return _hour / 24 + _minute / (24 * 60) + _second / (24 * 60 * 60); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TIMEVALUE + // ----------------------------------------------------------------------------- + const TIMEVALUE = { + description: _t("Converts a time string into its serial number representation."), + args: [arg("time_string (string)", _t("The string that holds the time representation."))], + returns: ["NUMBER"], + compute: function (timeString) { + const _timeString = toString(timeString); + const internalDate = parseDateTime(_timeString, this.locale); + assert(() => internalDate !== null, _t("The time_string (%s) cannot be parsed to date/time.", _timeString)); + const result = internalDate.value - Math.trunc(internalDate.value); + return result < 0 ? 1 + result : result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TODAY + // ----------------------------------------------------------------------------- + const TODAY = { + description: _t("Current date as a date value."), + args: [], + returns: ["DATE"], + computeFormat: function () { + return this.locale.dateFormat; + }, + compute: function () { + const today = new Date(); + const jsDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + return jsDateToRoundNumber(jsDate); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // WEEKDAY + // ----------------------------------------------------------------------------- + const WEEKDAY = { + description: _t("Day of the week of the date provided (as number)."), + args: [ + arg("date (date)", _t("The date for which to determine the day of the week. Must be a reference to a cell containing a date, a function returning a date type, or a number.")), + arg(`type (number, default=${DEFAULT_TYPE})`, _t("A number indicating which numbering system to use to represent weekdays. By default, counts starting with Sunday = 1.")), + ], + returns: ["NUMBER"], + compute: function (date, type = DEFAULT_TYPE) { + const _date = toJsDate(date, this.locale); + const _type = Math.round(toNumber(type, this.locale)); + const m = _date.getDay(); + assert(() => [1, 2, 3].includes(_type), _t("The type (%s) must be 1, 2 or 3.", _type.toString())); + if (_type === 1) + return m + 1; + if (_type === 2) + return m === 0 ? 7 : m; + return m === 0 ? 6 : m - 1; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // WEEKNUM + // ----------------------------------------------------------------------------- + const WEEKNUM = { + description: _t("Week number of the year."), + args: [ + arg("date (date)", _t("The date for which to determine the week number. Must be a reference to a cell containing a date, a function returning a date type, or a number.")), + arg(`type (number, default=${DEFAULT_TYPE})`, _t("A number representing the day that a week starts on. Sunday = 1.")), + ], + returns: ["NUMBER"], + compute: function (date, type = DEFAULT_TYPE) { + const _date = toJsDate(date, this.locale); + const _type = Math.round(toNumber(type, this.locale)); + assert(() => _type === 1 || _type === 2 || (11 <= _type && _type <= 17) || _type === 21, _t("The type (%s) is out of range.", _type.toString())); + if (_type === 21) { + return ISOWEEKNUM.compute.bind(this)(date); + } + let startDayOfWeek; + if (_type === 1 || _type === 2) { + startDayOfWeek = _type - 1; + } + else { + // case 11 <= _type <= 17 + startDayOfWeek = _type - 10 === 7 ? 0 : _type - 10; + } + const y = _date.getFullYear(); + let dayStart = 1; + let startDayOfFirstWeek = new Date(y, 0, dayStart); + while (startDayOfFirstWeek.getDay() !== startDayOfWeek) { + dayStart += 1; + startDayOfFirstWeek = new Date(y, 0, dayStart); + } + const dif = (_date.getTime() - startDayOfFirstWeek.getTime()) / MS_PER_DAY; + if (dif < 0) { + return 1; + } + return Math.floor(dif / 7) + (dayStart === 1 ? 1 : 2); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // WORKDAY + // ----------------------------------------------------------------------------- + const WORKDAY = { + description: _t("Date after a number of workdays."), + args: [ + arg("start_date (date)", _t("The date from which to begin counting.")), + arg("num_days (number)", _t("The number of working days to advance from start_date. If negative, counts backwards.")), + arg("holidays (date, range, optional)", _t("A range or array constant containing the dates to consider holidays.")), + ], + returns: ["NUMBER"], + computeFormat: function () { + return this.locale.dateFormat; + }, + compute: function (startDate, numDays, holidays = undefined) { + return WORKDAY_INTL.compute.bind(this)(startDate, numDays, 1, holidays ?? null); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // WORKDAY.INTL + // ----------------------------------------------------------------------------- + const WORKDAY_INTL = { + description: _t("Date after a number of workdays (specifying weekends)."), + args: [ + arg("start_date (date)", _t("The date from which to begin counting.")), + arg("num_days (number)", _t("The number of working days to advance from start_date. If negative, counts backwards.")), + arg(`weekend (any, default=${DEFAULT_WEEKEND})`, _t("A number or string representing which days of the week are considered weekends.")), + arg("holidays (date, range, optional)", _t("A range or array constant containing the dates to consider holidays.")), + ], + returns: ["DATE"], + computeFormat: function () { + return this.locale.dateFormat; + }, + compute: function (startDate, numDays, weekend = DEFAULT_WEEKEND, holidays) { + let _startDate = toJsDate(startDate, this.locale); + let _numDays = Math.trunc(toNumber(numDays, this.locale)); + if (typeof weekend === "string") { + assert(() => weekend !== "1111111", _t("The weekend (%s) must be different from '1111111'.", weekend)); + } + const daysWeekend = weekendToDayNumber(weekend); + let timesHoliday = new Set(); + if (holidays !== undefined) { + visitAny([holidays], (h) => { + const holiday = toJsDate(h, this.locale); + timesHoliday.add(holiday.getTime()); + }); + } + let stepDate = new Date(_startDate.getTime()); + let timeStepDate = stepDate.getTime(); + const unitDay = Math.sign(_numDays); + let stepDay = Math.abs(_numDays); + while (stepDay > 0) { + stepDate.setDate(stepDate.getDate() + unitDay); + timeStepDate = stepDate.getTime(); + if (!daysWeekend.includes(stepDate.getDay()) && !timesHoliday.has(timeStepDate)) { + stepDay -= 1; + } + } + const delta = timeStepDate - INITIAL_1900_DAY.getTime(); + return Math.round(delta / MS_PER_DAY); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // YEAR + // ----------------------------------------------------------------------------- + const YEAR = { + description: _t("Year specified by a given date."), + args: [arg("date (date)", _t("The date from which to extract the year."))], + returns: ["NUMBER"], + compute: function (date) { + return toJsDate(date, this.locale).getFullYear(); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // YEARFRAC + // ----------------------------------------------------------------------------- + const DEFAULT_DAY_COUNT_CONVENTION$1 = 0; + const YEARFRAC = { + description: _t("Exact number of years between two dates."), + args: [ + arg("start_date (date)", _t("The start date to consider in the calculation. Must be a reference to a cell containing a date, a function returning a date type, or a number.")), + arg("end_date (date)", _t("The end date to consider in the calculation. Must be a reference to a cell containing a date, a function returning a date type, or a number.")), + arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION$1})`, _t("An indicator of what day count method to use.")), + ], + returns: ["NUMBER"], + compute: function (startDate, endDate, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION$1) { + let _startDate = Math.trunc(toNumber(startDate, this.locale)); + let _endDate = Math.trunc(toNumber(endDate, this.locale)); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assert(() => _startDate >= 0, _t("The start_date (%s) must be positive or null.", _startDate.toString())); + assert(() => _endDate >= 0, _t("The end_date (%s) must be positive or null.", _endDate.toString())); + assert(() => 0 <= _dayCountConvention && _dayCountConvention <= 4, _t("The day_count_convention (%s) must be between 0 and 4 inclusive.", _dayCountConvention.toString())); + return getYearFrac(_startDate, _endDate, _dayCountConvention); + }, + }; + // ----------------------------------------------------------------------------- + // MONTH.START + // ----------------------------------------------------------------------------- + const MONTH_START = { + description: _t("First day of the month preceding a date."), + args: [arg("date (date)", _t("The date from which to calculate the result."))], + returns: ["DATE"], + computeFormat: function () { + return this.locale.dateFormat; + }, + compute: function (date) { + const _startDate = toJsDate(date, this.locale); + const yStart = _startDate.getFullYear(); + const mStart = _startDate.getMonth(); + const jsDate = new Date(yStart, mStart, 1); + return jsDateToRoundNumber(jsDate); + }, + }; + // ----------------------------------------------------------------------------- + // MONTH.END + // ----------------------------------------------------------------------------- + const MONTH_END = { + description: _t("Last day of the month following a date."), + args: [arg("date (date)", _t("The date from which to calculate the result."))], + returns: ["DATE"], + computeFormat: function () { + return this.locale.dateFormat; + }, + compute: function (date) { + return EOMONTH.compute.bind(this)(date, 0); + }, + }; + // ----------------------------------------------------------------------------- + // QUARTER + // ----------------------------------------------------------------------------- + const QUARTER = { + description: _t("Quarter of the year a specific date falls in"), + args: [arg("date (date)", _t("The date from which to extract the quarter."))], + returns: ["NUMBER"], + compute: function (date) { + return Math.ceil((toJsDate(date, this.locale).getMonth() + 1) / 3); + }, + }; + // ----------------------------------------------------------------------------- + // QUARTER.START + // ----------------------------------------------------------------------------- + const QUARTER_START = { + description: _t("First day of the quarter of the year a specific date falls in."), + args: [arg("date (date)", _t("The date from which to calculate the start of quarter."))], + returns: ["DATE"], + computeFormat: function () { + return this.locale.dateFormat; + }, + compute: function (date) { + const quarter = QUARTER.compute.bind(this)(date); + const year = YEAR.compute.bind(this)(date); + const jsDate = new Date(year, (quarter - 1) * 3, 1); + return jsDateToRoundNumber(jsDate); + }, + }; + // ----------------------------------------------------------------------------- + // QUARTER.END + // ----------------------------------------------------------------------------- + const QUARTER_END = { + description: _t("Last day of the quarter of the year a specific date falls in."), + args: [arg("date (date)", _t("The date from which to calculate the end of quarter."))], + returns: ["DATE"], + computeFormat: function () { + return this.locale.dateFormat; + }, + compute: function (date) { + const quarter = QUARTER.compute.bind(this)(date); + const year = YEAR.compute.bind(this)(date); + const jsDate = new Date(year, quarter * 3, 0); + return jsDateToRoundNumber(jsDate); + }, + }; + // ----------------------------------------------------------------------------- + // YEAR.START + // ----------------------------------------------------------------------------- + const YEAR_START = { + description: _t("First day of the year a specific date falls in."), + args: [arg("date (date)", _t("The date from which to calculate the start of the year."))], + returns: ["DATE"], + computeFormat: function () { + return this.locale.dateFormat; + }, + compute: function (date) { + const year = YEAR.compute.bind(this)(date); + const jsDate = new Date(year, 0, 1); + return jsDateToRoundNumber(jsDate); + }, + }; + // ----------------------------------------------------------------------------- + // YEAR.END + // ----------------------------------------------------------------------------- + const YEAR_END = { + description: _t("Last day of the year a specific date falls in."), + args: [arg("date (date)", _t("The date from which to calculate the end of the year."))], + returns: ["DATE"], + computeFormat: function () { + return this.locale.dateFormat; + }, + compute: function (date) { + const year = YEAR.compute.bind(this)(date); + const jsDate = new Date(year + 1, 0, 0); + return jsDateToRoundNumber(jsDate); + }, + }; + + var date = /*#__PURE__*/Object.freeze({ + __proto__: null, + DATE: DATE, + DATEDIF: DATEDIF, + DATEVALUE: DATEVALUE, + DAY: DAY, + DAYS: DAYS, + DAYS360: DAYS360, + EDATE: EDATE, + EOMONTH: EOMONTH, + HOUR: HOUR, + ISOWEEKNUM: ISOWEEKNUM, + MINUTE: MINUTE, + MONTH: MONTH, + MONTH_END: MONTH_END, + MONTH_START: MONTH_START, + NETWORKDAYS: NETWORKDAYS, + NETWORKDAYS_INTL: NETWORKDAYS_INTL, + NOW: NOW, + QUARTER: QUARTER, + QUARTER_END: QUARTER_END, + QUARTER_START: QUARTER_START, + SECOND: SECOND, + TIME: TIME, + TIMEVALUE: TIMEVALUE, + TODAY: TODAY, + WEEKDAY: WEEKDAY, + WEEKNUM: WEEKNUM, + WORKDAY: WORKDAY, + WORKDAY_INTL: WORKDAY_INTL, + YEAR: YEAR, + YEARFRAC: YEARFRAC, + YEAR_END: YEAR_END, + YEAR_START: YEAR_START + }); + + const DEFAULT_DELTA_ARG = 0; + // ----------------------------------------------------------------------------- + // DELTA + // ----------------------------------------------------------------------------- + const DELTA = { + description: _t("Compare two numeric values, returning 1 if they're equal."), + args: [ + arg(" (number)", _t("The first number to compare.")), + arg(` (number, default=${DEFAULT_DELTA_ARG})`, _t("The second number to compare.")), + ], + returns: ["NUMBER"], + compute: function (number1, number2 = DEFAULT_DELTA_ARG) { + const _number1 = toNumber(number1, this.locale); + const _number2 = toNumber(number2, this.locale); + return _number1 === _number2 ? 1 : 0; + }, + isExported: true, + }; + + var engineering = /*#__PURE__*/Object.freeze({ + __proto__: null, + DELTA: DELTA + }); + + const SORT_TYPES = [ + CellValueType.number, + CellValueType.error, + CellValueType.text, + CellValueType.boolean, + ]; + function cellsSortingCriterion(sortingOrder) { + const inverse = sortingOrder === "ascending" ? 1 : -1; + return (left, right) => { + if (left.type === CellValueType.empty) { + return right.type === CellValueType.empty ? 0 : 1; + } + else if (right.type === CellValueType.empty) { + return -1; + } + let typeOrder = SORT_TYPES.indexOf(left.type) - SORT_TYPES.indexOf(right.type); + if (typeOrder === 0) { + if (left.type === CellValueType.text || left.type === CellValueType.error) { + typeOrder = left.value.localeCompare(right.value); + } + else { + typeOrder = left.value - right.value; + } + } + return inverse * typeOrder; + }; + } + function sortCells(cells, sortDirection, emptyCellAsZero) { + const cellsWithIndex = cells.map((cell, index) => ({ + index, + type: cell.type, + value: cell.value, + })); + const cellsToSort = emptyCellAsZero + ? cellsWithIndex.map((cell) => cell.type === CellValueType.empty ? { ...cell, type: CellValueType.number, value: 0 } : cell) + : cellsWithIndex; + return cellsToSort.sort(cellsSortingCriterion(sortDirection)); + } + function interactiveSortSelection(env, sheetId, anchor, zone, sortDirection) { + let result = DispatchResult.Success; + //several columns => bypass the contiguity check + let multiColumns = zone.right > zone.left; + if (env.model.getters.doesIntersectMerge(sheetId, zone)) { + multiColumns = false; + let table; + for (let row = zone.top; row <= zone.bottom; row++) { + table = []; + for (let col = zone.left; col <= zone.right; col++) { + let merge = env.model.getters.getMerge({ sheetId, col, row }); + if (merge && !table.includes(merge.id.toString())) { + table.push(merge.id.toString()); + } + } + if (table.length >= 2) { + multiColumns = true; + break; + } + } + } + const { col, row } = anchor; + if (multiColumns) { + result = env.model.dispatch("SORT_CELLS", { sheetId, col, row, zone, sortDirection }); + } + else { + // check contiguity + const contiguousZone = env.model.getters.getContiguousZone(sheetId, zone); + if (isEqual(contiguousZone, zone)) { + // merge as it is + result = env.model.dispatch("SORT_CELLS", { + sheetId, + col, + row, + zone, + sortDirection, + }); + } + else { + env.askConfirmation(_t("We found data next to your selection. Since this data was not selected, it will not be sorted. Do you want to extend your selection?"), () => { + zone = contiguousZone; + result = env.model.dispatch("SORT_CELLS", { + sheetId, + col, + row, + zone, + sortDirection, + }); + }, () => { + result = env.model.dispatch("SORT_CELLS", { + sheetId, + col, + row, + zone, + sortDirection, + }); + }); + } + } + if (result.isCancelledBecause("InvalidSortZone" /* CommandResult.InvalidSortZone */)) { + const { col, row } = anchor; + env.model.selection.selectZone({ cell: { col, row }, zone }); + env.raiseError(_t("Cannot sort. To sort, select only cells or only merges that have the same size.")); + } + } + + function sortMatrix(matrix, locale, ...criteria) { + for (const [i, value] of criteria.entries()) { + assert(() => value !== undefined, _t("Value for parameter %d is missing, while the function [[FUNCTION_NAME]] expect a number or a range.", i + 1)); + } + const sortingOrders = []; + const sortColumns = []; + const nRows = matrix.length; + for (let i = 0; i < criteria.length; i += 2) { + sortingOrders.push(toBoolean(toScalar(criteria[i + 1])?.value) ? "ascending" : "descending"); + const sortColumn = criteria[i]; + if (isMatrix(sortColumn) && (sortColumn.length > 1 || sortColumn[0].length > 1)) { + assert(() => sortColumn.length === 1 && sortColumn[0].length === nRows, _t("Wrong size for %s. Expected a range of size 1x%s. Got %sx%s.", `sort_column${i + 1}`, nRows, sortColumn.length, sortColumn[0].length)); + sortColumns.push(sortColumn.flat().map((c) => c.value)); + } + else { + const colIndex = toNumber(toScalar(sortColumn)?.value, locale); + if (colIndex < 1 || colIndex > matrix[0].length) { + return matrix; + } + sortColumns.push(matrix.map((row) => row[colIndex - 1].value)); + } + } + if (sortColumns.length === 0) { + for (let i = 0; i < matrix[0].length; i++) { + sortColumns.push(matrix.map((row) => row[i].value)); + sortingOrders.push("ascending"); + } + } + const sortingCriteria = { + descending: cellsSortingCriterion("descending"), + ascending: cellsSortingCriterion("ascending"), + }; + const indexes = range(0, matrix.length); + indexes.sort((a, b) => { + for (const [i, sortColumn] of sortColumns.entries()) { + const left = sortColumn[a]; + const right = sortColumn[b]; + const leftCell = { + value: left, + type: left === null + ? CellValueType.empty + : typeof left === "string" + ? CellValueType.text + : typeof left, + }; + const rightCell = { + value: right, + type: right === null + ? CellValueType.empty + : typeof right === "string" + ? CellValueType.text + : typeof right, + }; + const result = sortingCriteria[sortingOrders[i]](leftCell, rightCell); + if (result !== 0) { + return result; + } + } + return 0; + }); + return indexes.map((i) => matrix[i]); + } + // ----------------------------------------------------------------------------- + // FILTER + // ----------------------------------------------------------------------------- + const FILTER = { + description: _t("Returns a filtered version of the source range, returning only rows or columns that meet the specified conditions."), + // TODO modify args description when vectorization on formulas is available + args: [ + arg("range (any, range)", _t("The data to be filtered.")), + arg("condition1 (boolean, range)", _t("A column or row containing true or false values corresponding to the first column or row of range.")), + arg("condition2 (boolean, range, repeating)", _t("Additional column or row containing true or false values.")), + ], + returns: ["RANGE"], + computeValueAndFormat: function (range, ...conditions) { + let _array = toMatrix(range); + const _conditionsMatrices = conditions.map((cond) => matrixMap(toMatrix(cond), (data) => data.value)); + _conditionsMatrices.map((c) => assertSingleColOrRow(_t("The arguments condition must be a single column or row."), c)); + assertSameDimensions(_t("The arguments conditions must have the same dimensions."), ..._conditionsMatrices); + const _conditions = _conditionsMatrices.map((c) => c.flat()); + const mode = _conditionsMatrices[0].length === 1 ? "row" : "col"; + _array = mode === "row" ? transposeMatrix(_array) : _array; + assert(() => _conditions.every((cond) => cond.length === _array.length), _t("FILTER has mismatched sizes on the range and conditions.")); + const result = []; + for (let i = 0; i < _array.length; i++) { + const row = _array[i]; + if (_conditions.every((c) => c[i])) { + result.push(row); + } + } + if (!result.length) { + throw new NotAvailableError(_t("No match found in FILTER evaluation")); + } + return mode === "row" ? transposeMatrix(result) : result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SORT + // ----------------------------------------------------------------------------- + const SORT = { + description: _t("Sorts the rows of a given array or range by the values in one or more columns."), + args: [ + arg("range (range)", _t("The data to be sorted.")), + arg("sort_column (any, range, repeating)", _t("The index of the column in range or a range outside of range containing the values by which to sort.")), + arg("is_ascending (boolean, repeating)", _t("TRUE or FALSE indicating whether to sort sort_column in ascending order. FALSE sorts in descending order.")), + ], + returns: ["RANGE"], + computeValueAndFormat: function (range, ...sortingCriteria) { + const _range = transposeMatrix(range); + return transposeMatrix(sortMatrix(_range, this.locale, ...sortingCriteria)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SORTN + // ----------------------------------------------------------------------------- + const SORTN = { + description: _t("Returns the first n items in a data set after performing a sort."), + args: [ + arg("range (range)", _t("The data to be sorted.")), + arg("n (number, default=1)", _t("The number of items to return.")), + arg("display_ties_mode (number, default=0)", _t("A number representing the way to display ties.")), + arg("sort_column (number, range, repeating)", _t("The index of the column in range or a range outside of range containing the values by which to sort.")), + arg("is_ascending (boolean, repeating)", _t("TRUE or FALSE indicating whether to sort sort_column in ascending order. FALSE sorts in descending order.")), + ], + returns: ["RANGE"], + computeValueAndFormat: function (range, n, displayTiesMode, ...sortingCriteria) { + const _n = toNumber(n?.value ?? 1, this.locale); + assert(() => _n >= 0, _t("Wrong value of 'n'. Expected a positive number. Got %s.", _n)); + const _displayTiesMode = toNumber(displayTiesMode?.value ?? 0, this.locale); + assert(() => _displayTiesMode >= 0 && _displayTiesMode <= 3, _t("Wrong value of 'display_ties_mode'. Expected a positive number between 0 and 3. Got %s.", _displayTiesMode)); + const sortedData = sortMatrix(transposeMatrix(range), this.locale, ...sortingCriteria); + const sameRows = (i, j) => JSON.stringify(sortedData[i].map((c) => c.value)) === + JSON.stringify(sortedData[j].map((c) => c.value)); + /* + * displayTiesMode determine how ties (equal values) are dealt with: + * 0 - ignore ties and show first n rows only + * 1 - show first n rows plus any additional ties with nth row + * 2 - show n rows but remove duplicates + * 3 - show first n unique rows and all duplicates of these rows + */ + switch (_displayTiesMode) { + case 0: + return transposeMatrix(sortedData.slice(0, _n)); + case 1: + for (let i = _n; i < sortedData.length; i++) { + if (!sameRows(i, _n - 1)) { + return transposeMatrix(sortedData.slice(0, i)); + } + } + return transposeMatrix(sortedData); + case 2: { + const uniques = [sortedData[0]]; + for (let i = 1; i < sortedData.length; i++) { + for (let j = 0; j < i; j++) { + if (sameRows(i, j)) { + break; + } + if (j === i - 1) { + uniques.push(sortedData[i]); + } + } + } + return transposeMatrix(uniques.slice(0, _n)); + } + case 3: { + const uniques = [sortedData[0]]; + let counter = 1; + for (let i = 1; i < sortedData.length; i++) { + if (!sameRows(i, i - 1)) { + counter++; + } + if (counter > _n) { + break; + } + uniques.push(sortedData[i]); + } + return transposeMatrix(uniques); + } + } + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // UNIQUE + // ----------------------------------------------------------------------------- + const UNIQUE = { + description: _t("Unique rows in the provided source range."), + args: [ + arg("range (any, range)", _t("The data to filter by unique entries.")), + arg("by_column (boolean, default=FALSE)", _t("Whether to filter the data by columns or by rows.")), + arg("exactly_once (boolean, default=FALSE)", _t("Whether to return only entries with no duplicates.")), + ], + returns: ["RANGE"], + computeValueAndFormat: function (range = { value: "" }, byColumn, exactlyOnce) { + if (!isMatrix(range)) { + return [[range]]; + } + const _byColumn = toBoolean(byColumn?.value) || false; + const _exactlyOnce = toBoolean(exactlyOnce?.value) || false; + if (!_byColumn) { + range = transposeMatrix(range); + } + const map = new Map(); + for (const data of range) { + const key = JSON.stringify(data.map((item) => item.value)); + const occurrence = map.get(key); + if (!occurrence) { + map.set(key, { data, count: 1 }); + } + else { + occurrence.count++; + } + } + const result = []; + for (const row of map.values()) { + if (_exactlyOnce && row.count > 1) { + continue; + } + result.push(row.data); + } + if (!result.length) + throw new Error(_t("No unique values found")); + return _byColumn ? result : transposeMatrix(result); + }, + isExported: true, + }; + + var filter = /*#__PURE__*/Object.freeze({ + __proto__: null, + FILTER: FILTER, + SORT: SORT, + SORTN: SORTN, + UNIQUE: UNIQUE + }); + + /** Assert maturity date > settlement date */ + function assertMaturityAndSettlementDatesAreValid(settlement, maturity) { + assert(() => settlement < maturity, _t("The maturity (%s) must be strictly greater than the settlement (%s).", maturity.toString(), settlement.toString())); + } + /** Assert settlement date > issue date */ + function assertSettlementAndIssueDatesAreValid(settlement, issue) { + assert(() => issue < settlement, _t("The settlement date (%s) must be strictly greater than the issue date (%s).", settlement.toString(), issue.toString())); + } + /** Assert coupon frequency is in [1, 2, 4] */ + function assertCouponFrequencyIsValid(frequency) { + assert(() => [1, 2, 4].includes(frequency), _t("The frequency (%s) must be one of %s", frequency.toString(), [1, 2, 4].toString())); + } + /** Assert dayCountConvention is between 0 and 4 */ + function assertDayCountConventionIsValid(dayCountConvention) { + assert(() => 0 <= dayCountConvention && dayCountConvention <= 4, _t("The day_count_convention (%s) must be between 0 and 4 inclusive.", dayCountConvention.toString())); + } + function assertRedemptionStrictlyPositive(redemption) { + assert(() => redemption > 0, _t("The redemption (%s) must be strictly positive.", redemption.toString())); + } + function assertPriceStrictlyPositive(price) { + assert(() => price > 0, _t("The price (%s) must be strictly positive.", price.toString())); + } + function assertNumberOfPeriodsStrictlyPositive(nPeriods) { + assert(() => nPeriods > 0, _t("The number_of_periods (%s) must be greater than 0.", nPeriods.toString())); + } + function assertRateStrictlyPositive(rate) { + assert(() => rate > 0, _t("The rate (%s) must be strictly positive.", rate.toString())); + } + function assertLifeStrictlyPositive(life) { + assert(() => life > 0, _t("The life (%s) must be strictly positive.", life.toString())); + } + function assertCostStrictlyPositive(cost) { + assert(() => cost > 0, _t("The cost (%s) must be strictly positive.", cost.toString())); + } + function assertCostPositiveOrZero(cost) { + assert(() => cost >= 0, _t("The cost (%s) must be positive or null.", cost.toString())); + } + function assertPeriodStrictlyPositive(period) { + assert(() => period > 0, _t("The period (%s) must be strictly positive.", period.toString())); + } + function assertPeriodPositiveOrZero(period) { + assert(() => period >= 0, _t("The period (%s) must be positive or null.", period.toString())); + } + function assertSalvagePositiveOrZero(salvage) { + assert(() => salvage >= 0, _t("The salvage (%s) must be positive or null.", salvage.toString())); + } + function assertSalvageSmallerOrEqualThanCost(salvage, cost) { + assert(() => salvage <= cost, _t("The salvage (%s) must be smaller or equal than the cost (%s).", salvage.toString(), cost.toString())); + } + function assertPresentValueStrictlyPositive(pv) { + assert(() => pv > 0, _t("The present value (%s) must be strictly positive.", pv.toString())); + } + function assertPeriodSmallerOrEqualToLife(period, life) { + assert(() => period <= life, _t("The period (%s) must be less than or equal life (%s).", period.toString(), life.toString())); + } + function assertInvestmentStrictlyPositive(investment) { + assert(() => investment > 0, _t("The investment (%s) must be strictly positive.", investment.toString())); + } + function assertDiscountStrictlyPositive(discount) { + assert(() => discount > 0, _t("The discount (%s) must be strictly positive.", discount.toString())); + } + function assertDiscountStrictlySmallerThanOne(discount) { + assert(() => discount < 1, _t("The discount (%s) must be smaller than 1.", discount.toString())); + } + function assertDeprecationFactorStrictlyPositive(factor) { + assert(() => factor > 0, _t("The depreciation factor (%s) must be strictly positive.", factor.toString())); + } + function assertSettlementLessThanOneYearBeforeMaturity(settlement, maturity, locale) { + const startDate = toJsDate(settlement, locale); + const endDate = toJsDate(maturity, locale); + const startDatePlusOneYear = new Date(startDate); + startDatePlusOneYear.setFullYear(startDate.getFullYear() + 1); + assert(() => endDate.getTime() <= startDatePlusOneYear.getTime(), _t("The settlement date (%s) must at most one year after the maturity date (%s).", settlement.toString(), maturity.toString())); + } + /** + * Check if the given periods are valid. This will assert : + * + * - 0 < numberOfPeriods + * - 0 < firstPeriod <= lastPeriod + * - 0 < lastPeriod <= numberOfPeriods + * + */ + function assertFirstAndLastPeriodsAreValid(firstPeriod, lastPeriod, numberOfPeriods) { + assertNumberOfPeriodsStrictlyPositive(numberOfPeriods); + assert(() => firstPeriod > 0, _t("The first_period (%s) must be strictly positive.", firstPeriod.toString())); + assert(() => lastPeriod > 0, _t("The last_period (%s) must be strictly positive.", lastPeriod.toString())); + assert(() => firstPeriod <= lastPeriod, _t("The first_period (%s) must be smaller or equal to the last_period (%s).", firstPeriod.toString(), lastPeriod.toString())); + assert(() => lastPeriod <= numberOfPeriods, _t("The last_period (%s) must be smaller or equal to the number_of_periods (%s).", firstPeriod.toString(), numberOfPeriods.toString())); + } + /** + * Check if the given periods are valid. This will assert : + * + * - 0 < life + * - 0 <= startPeriod <= endPeriod + * - 0 <= endPeriod <= life + * + */ + function assertStartAndEndPeriodAreValid(startPeriod, endPeriod, life) { + assertLifeStrictlyPositive(life); + assert(() => startPeriod >= 0, _t("The start_period (%s) must be greater or equal than 0.", startPeriod.toString())); + assert(() => endPeriod >= 0, _t("The end_period (%s) must be greater or equal than 0.", endPeriod.toString())); + assert(() => startPeriod <= endPeriod, _t("The start_period (%s) must be smaller or equal to the end_period (%s).", startPeriod.toString(), endPeriod.toString())); + assert(() => endPeriod <= life, _t("The end_period (%s) must be smaller or equal to the life (%s).", startPeriod.toString(), life.toString())); + } + function assertRateGuessStrictlyGreaterThanMinusOne(guess) { + assert(() => guess > -1, _t("The rate_guess (%s) must be strictly greater than -1.", guess.toString())); + } + function assertCashFlowsAndDatesHaveSameDimension(cashFlows, dates) { + assert(() => cashFlows.length === dates.length && cashFlows[0].length === dates[0].length, _t("The cashflow_amounts and cashflow_dates ranges must have the same dimensions.")); + } + function assertCashFlowsHavePositiveAndNegativesValues(cashFlow) { + assert(() => cashFlow.some((val) => val > 0) && cashFlow.some((val) => val < 0), _t("There must be both positive and negative values in cashflow_amounts.")); + } + function assertEveryDateGreaterThanFirstDateOfCashFlowDates(dates) { + assert(() => dates.every((date) => date >= dates[0]), _t("All the dates should be greater or equal to the first date in cashflow_dates (%s).", dates[0].toString())); + } + + const DEFAULT_DAY_COUNT_CONVENTION = 0; + const DEFAULT_END_OR_BEGINNING = 0; + const DEFAULT_FUTURE_VALUE = 0; + const COUPON_FUNCTION_ARGS = [ + arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), + arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), + arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4).")), + arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), + ]; + /** + * Use the Newton–Raphson method to find a root of the given function in an iterative manner. + * + * @param func the function to find a root of + * @param derivFunc the derivative of the function + * @param startValue the initial value for the first iteration of the algorithm + * @param maxIterations the maximum number of iterations + * @param epsMax the epsilon for the root + * @param nanFallback a function giving a fallback value to use if func(x) returns NaN. Useful if the + * function is not defined for some range, but we know approximately where the root is when the Newton + * algorithm ends up in this range. + */ + function newtonMethod(func, derivFunc, startValue, maxIterations, epsMax = 1e-10, nanFallback) { + let x = startValue; + let newX; + let xDelta; + let y; + let yEqual0 = false; + let count = 0; + let previousFallback = undefined; + do { + y = func(x); + if (isNaN(y)) { + assert(() => count < maxIterations && nanFallback !== undefined, _t("Function [[FUNCTION_NAME]] didn't find any result.")); + count++; + x = nanFallback(previousFallback); + previousFallback = x; + continue; + } + newX = x - y / derivFunc(x); + xDelta = Math.abs(newX - x); + x = newX; + yEqual0 = xDelta < epsMax || Math.abs(y) < epsMax; + assert(() => count < maxIterations, _t("Function [[FUNCTION_NAME]] didn't find any result.")); + count++; + } while (!yEqual0); + return x; + } + // ----------------------------------------------------------------------------- + // ACCRINTM + // ----------------------------------------------------------------------------- + const ACCRINTM = { + description: _t("Accrued interest of security paying at maturity."), + args: [ + arg("issue (date)", _t("The date the security was initially issued.")), + arg("maturity (date)", _t("The maturity date of the security.")), + arg("rate (number)", _t("The annualized rate of interest.")), + arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")), + arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), + ], + returns: ["NUMBER"], + compute: function (issue, maturity, rate, redemption, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const start = Math.trunc(toNumber(issue, this.locale)); + const end = Math.trunc(toNumber(maturity, this.locale)); + const _redemption = toNumber(redemption, this.locale); + const _rate = toNumber(rate, this.locale); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertSettlementAndIssueDatesAreValid(end, start); + assertDayCountConventionIsValid(_dayCountConvention); + assertRedemptionStrictlyPositive(_redemption); + assertRateStrictlyPositive(_rate); + const yearFrac = YEARFRAC.compute.bind(this)(start, end, dayCountConvention); + return _redemption * _rate * yearFrac; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // AMORLINC + // ----------------------------------------------------------------------------- + const AMORLINC = { + description: _t("Depreciation for an accounting period."), + args: [ + arg("cost (number)", _t("The initial cost of the asset.")), + arg("purchase_date (date)", _t("The date the asset was purchased.")), + arg("first_period_end (date)", _t("The date the first period ended.")), + arg("salvage (number)", _t("The value of the asset at the end of depreciation.")), + arg("period (number)", _t("The single period within life for which to calculate depreciation.")), + arg("rate (number)", _t("The deprecation rate.")), + arg(" (number, optional)", _t("An indicator of what day count method to use.")), + ], + returns: ["NUMBER"], + compute: function (cost, purchaseDate, firstPeriodEnd, salvage, period, rate, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const _cost = toNumber(cost, this.locale); + const _purchaseDate = Math.trunc(toNumber(purchaseDate, this.locale)); + const _firstPeriodEnd = Math.trunc(toNumber(firstPeriodEnd, this.locale)); + const _salvage = toNumber(salvage, this.locale); + const _period = toNumber(period, this.locale); + const _rate = toNumber(rate, this.locale); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertCostStrictlyPositive(_cost); + assertSalvagePositiveOrZero(_salvage); + assertSalvageSmallerOrEqualThanCost(_salvage, _cost); + assertPeriodPositiveOrZero(_period); + assertRateStrictlyPositive(_rate); + assertDayCountConventionIsValid(_dayCountConvention); + assert(() => _purchaseDate <= _firstPeriodEnd, _t("The purchase_date (%s) must be before the first_period_end (%s).", _purchaseDate.toString(), _firstPeriodEnd.toString())); + /** + * https://wiki.documentfoundation.org/Documentation/Calc_Functions/AMORLINC + * + * AMORLINC period 0 = cost * rate * YEARFRAC(purchase date, first period end) + * AMORLINC period n = cost * rate + * AMORLINC at the last period is such that the remaining deprecated cost is equal to the salvage value. + * + * The period is and rounded to 1 if < 1 truncated if > 1, + * + * Compatibility note : + * If (purchase date) === (first period end), on GSheet the deprecation at the first period is 0, and on Excel + * it is a full period deprecation. We choose to use the Excel behaviour. + */ + const roundedPeriod = _period < 1 && _period > 0 ? 1 : Math.trunc(_period); + const deprec = _cost * _rate; + const yearFrac = YEARFRAC.compute.bind(this)(_purchaseDate, _firstPeriodEnd, _dayCountConvention); + const firstDeprec = _purchaseDate === _firstPeriodEnd ? deprec : deprec * yearFrac; + const valueAtPeriod = _cost - firstDeprec - deprec * roundedPeriod; + if (valueAtPeriod >= _salvage) { + return roundedPeriod === 0 ? firstDeprec : deprec; + } + return _salvage - valueAtPeriod < deprec ? deprec - (_salvage - valueAtPeriod) : 0; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COUPDAYS + // ----------------------------------------------------------------------------- + const COUPDAYS = { + description: _t("Days in coupon period containing settlement date."), + args: COUPON_FUNCTION_ARGS, + returns: ["NUMBER"], + compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const start = Math.trunc(toNumber(settlement, this.locale)); + const end = Math.trunc(toNumber(maturity, this.locale)); + const _frequency = Math.trunc(toNumber(frequency, this.locale)); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertMaturityAndSettlementDatesAreValid(start, end); + assertCouponFrequencyIsValid(_frequency); + assertDayCountConventionIsValid(_dayCountConvention); + // https://wiki.documentfoundation.org/Documentation/Calc_Functions/COUPDAYS + if (_dayCountConvention === 1) { + const before = COUPPCD.compute.bind(this)(settlement, maturity, frequency, dayCountConvention); + const after = COUPNCD.compute.bind(this)(settlement, maturity, frequency, dayCountConvention); + return after - before; + } + const daysInYear = _dayCountConvention === 3 ? 365 : 360; + return daysInYear / _frequency; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COUPDAYBS + // ----------------------------------------------------------------------------- + const COUPDAYBS = { + description: _t("Days from settlement until next coupon."), + args: COUPON_FUNCTION_ARGS, + returns: ["NUMBER"], + compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const start = Math.trunc(toNumber(settlement, this.locale)); + const end = Math.trunc(toNumber(maturity, this.locale)); + const _frequency = Math.trunc(toNumber(frequency, this.locale)); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertMaturityAndSettlementDatesAreValid(start, end); + assertCouponFrequencyIsValid(_frequency); + assertDayCountConventionIsValid(_dayCountConvention); + const couponBeforeStart = COUPPCD.compute.bind(this)(start, end, frequency, dayCountConvention); + if ([1, 2, 3].includes(_dayCountConvention)) { + return start - couponBeforeStart; + } + if (_dayCountConvention === 4) { + const yearFrac = getYearFrac(couponBeforeStart, start, _dayCountConvention); + return Math.round(yearFrac * 360); + } + const startDate = toJsDate(start, this.locale); + const dateCouponBeforeStart = toJsDate(couponBeforeStart, this.locale); + const y1 = dateCouponBeforeStart.getFullYear(); + const y2 = startDate.getFullYear(); + const m1 = dateCouponBeforeStart.getMonth() + 1; // +1 because months in js start at 0 and it's confusing + const m2 = startDate.getMonth() + 1; + let d1 = dateCouponBeforeStart.getDate(); + let d2 = startDate.getDate(); + /** + * Rules based on https://en.wikipedia.org/wiki/Day_count_convention#30/360_US + * + * These are slightly modified (no mention of if investment is EOM and rules order is modified), + * but from my testing this seems the rules used by Excel/GSheet. + */ + if (m1 === 2 && + m2 === 2 && + isLastDayOfMonth(dateCouponBeforeStart) && + isLastDayOfMonth(startDate)) { + d2 = 30; + } + if (d2 === 31 && (d1 === 30 || d1 === 31)) { + d2 = 30; + } + if (m1 === 2 && isLastDayOfMonth(dateCouponBeforeStart)) { + d1 = 30; + } + if (d1 === 31) { + d1 = 30; + } + return (y2 - y1) * 360 + (m2 - m1) * 30 + (d2 - d1); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COUPDAYSNC + // ----------------------------------------------------------------------------- + const COUPDAYSNC = { + description: _t("Days from settlement until next coupon."), + args: COUPON_FUNCTION_ARGS, + returns: ["NUMBER"], + compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const start = Math.trunc(toNumber(settlement, this.locale)); + const end = Math.trunc(toNumber(maturity, this.locale)); + const _frequency = Math.trunc(toNumber(frequency, this.locale)); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertMaturityAndSettlementDatesAreValid(start, end); + assertCouponFrequencyIsValid(_frequency); + assertDayCountConventionIsValid(_dayCountConvention); + const couponAfterStart = COUPNCD.compute.bind(this)(start, end, frequency, dayCountConvention); + if ([1, 2, 3].includes(_dayCountConvention)) { + return couponAfterStart - start; + } + if (_dayCountConvention === 4) { + const yearFrac = getYearFrac(start, couponAfterStart, _dayCountConvention); + return Math.round(yearFrac * 360); + } + const coupDayBs = COUPDAYBS.compute.bind(this)(settlement, maturity, frequency, _dayCountConvention); + const coupDays = COUPDAYS.compute.bind(this)(settlement, maturity, frequency, _dayCountConvention); + return coupDays - coupDayBs; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COUPNCD + // ----------------------------------------------------------------------------- + const COUPNCD = { + description: _t("Next coupon date after the settlement date."), + args: COUPON_FUNCTION_ARGS, + returns: ["NUMBER"], + computeFormat: function () { + return this.locale.dateFormat; + }, + compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const start = Math.trunc(toNumber(settlement, this.locale)); + const end = Math.trunc(toNumber(maturity, this.locale)); + const _frequency = Math.trunc(toNumber(frequency, this.locale)); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertMaturityAndSettlementDatesAreValid(start, end); + assertCouponFrequencyIsValid(_frequency); + assertDayCountConventionIsValid(_dayCountConvention); + const monthsPerPeriod = 12 / _frequency; + const coupNum = COUPNUM.compute.bind(this)(settlement, maturity, frequency, dayCountConvention); + const date = addMonthsToDate(toJsDate(end, this.locale), -(coupNum - 1) * monthsPerPeriod, true); + return jsDateToRoundNumber(date); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COUPNUM + // ----------------------------------------------------------------------------- + const COUPNUM = { + description: _t("Number of coupons between settlement and maturity."), + args: COUPON_FUNCTION_ARGS, + returns: ["NUMBER"], + compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const start = Math.trunc(toNumber(settlement, this.locale)); + const end = Math.trunc(toNumber(maturity, this.locale)); + const _frequency = Math.trunc(toNumber(frequency, this.locale)); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertMaturityAndSettlementDatesAreValid(start, end); + assertCouponFrequencyIsValid(_frequency); + assertDayCountConventionIsValid(_dayCountConvention); + let num = 1; + let currentDate = end; + const monthsPerPeriod = 12 / _frequency; + while (currentDate > start) { + currentDate = jsDateToRoundNumber(addMonthsToDate(toJsDate(currentDate, this.locale), -monthsPerPeriod, false)); + num++; + } + return num - 1; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COUPPCD + // ----------------------------------------------------------------------------- + const COUPPCD = { + description: _t("Last coupon date prior to or on the settlement date."), + args: COUPON_FUNCTION_ARGS, + returns: ["NUMBER"], + computeFormat: function () { + return this.locale.dateFormat; + }, + compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const start = Math.trunc(toNumber(settlement, this.locale)); + const end = Math.trunc(toNumber(maturity, this.locale)); + const _frequency = Math.trunc(toNumber(frequency, this.locale)); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertMaturityAndSettlementDatesAreValid(start, end); + assertCouponFrequencyIsValid(_frequency); + assertDayCountConventionIsValid(_dayCountConvention); + const monthsPerPeriod = 12 / _frequency; + const coupNum = COUPNUM.compute.bind(this)(settlement, maturity, frequency, dayCountConvention); + const date = addMonthsToDate(toJsDate(end, this.locale), -coupNum * monthsPerPeriod, true); + return jsDateToRoundNumber(date); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // CUMIPMT + // ----------------------------------------------------------------------------- + const CUMIPMT = { + description: _t("Cumulative interest paid over a set of periods."), + args: [ + arg("rate (number)", _t("The interest rate.")), + arg("number_of_periods (number)", _t("The number of payments to be made.")), + arg("present_value (number)", _t("The current value of the annuity.")), + arg("first_period (number)", _t("The number of the payment period to begin the cumulative calculation.")), + arg("last_period (number)", _t("The number of the payment period to end the cumulative calculation.")), + arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), + ], + returns: ["NUMBER"], + compute: function (rate, numberOfPeriods, presentValue, firstPeriod, lastPeriod, endOrBeginning = DEFAULT_END_OR_BEGINNING) { + const first = toNumber(firstPeriod, this.locale); + const last = toNumber(lastPeriod, this.locale); + const _rate = toNumber(rate, this.locale); + const pv = toNumber(presentValue, this.locale); + const nOfPeriods = toNumber(numberOfPeriods, this.locale); + assertFirstAndLastPeriodsAreValid(first, last, nOfPeriods); + assertRateStrictlyPositive(_rate); + assertPresentValueStrictlyPositive(pv); + let cumSum = 0; + for (let i = first; i <= last; i++) { + const impt = IPMT.compute.bind(this)(rate, i, nOfPeriods, presentValue, 0, endOrBeginning); + cumSum += impt; + } + return cumSum; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // CUMPRINC + // ----------------------------------------------------------------------------- + const CUMPRINC = { + description: _t("Cumulative principal paid over a set of periods."), + args: [ + arg("rate (number)", _t("The interest rate.")), + arg("number_of_periods (number)", _t("The number of payments to be made.")), + arg("present_value (number)", _t("The current value of the annuity.")), + arg("first_period (number)", _t("The number of the payment period to begin the cumulative calculation.")), + arg("last_period (number)", _t("The number of the payment period to end the cumulative calculation.")), + arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), + ], + returns: ["NUMBER"], + compute: function (rate, numberOfPeriods, presentValue, firstPeriod, lastPeriod, endOrBeginning = DEFAULT_END_OR_BEGINNING) { + const first = toNumber(firstPeriod, this.locale); + const last = toNumber(lastPeriod, this.locale); + const _rate = toNumber(rate, this.locale); + const pv = toNumber(presentValue, this.locale); + const nOfPeriods = toNumber(numberOfPeriods, this.locale); + assertFirstAndLastPeriodsAreValid(first, last, nOfPeriods); + assertRateStrictlyPositive(_rate); + assertPresentValueStrictlyPositive(pv); + let cumSum = 0; + for (let i = first; i <= last; i++) { + const ppmt = PPMT.compute.bind(this)(rate, i, nOfPeriods, presentValue, 0, endOrBeginning); + cumSum += ppmt; + } + return cumSum; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DB + // ----------------------------------------------------------------------------- + const DB = { + description: _t("Depreciation via declining balance method."), + args: [ + arg("cost (number)", _t("The initial cost of the asset.")), + arg("salvage (number)", _t("The value of the asset at the end of depreciation.")), + arg("life (number)", _t("The number of periods over which the asset is depreciated.")), + arg("period (number)", _t("The single period within life for which to calculate depreciation.")), + arg("month (number, optional)", _t("The number of months in the first year of depreciation.")), + ], + returns: ["NUMBER"], + // to do: replace by dollar format + computeFormat: () => "#,##0.00", + compute: function (cost, salvage, life, period, ...args) { + const _cost = toNumber(cost, this.locale); + const _salvage = toNumber(salvage, this.locale); + const _life = toNumber(life, this.locale); + const _period = Math.trunc(toNumber(period, this.locale)); + const _month = args.length ? Math.trunc(toNumber(args[0], this.locale)) : 12; + const lifeLimit = _life + (_month === 12 ? 0 : 1); + assertCostPositiveOrZero(_cost); + assertSalvagePositiveOrZero(_salvage); + assertPeriodStrictlyPositive(_period); + assertLifeStrictlyPositive(_life); + assert(() => 1 <= _month && _month <= 12, _t("The month (%s) must be between 1 and 12 inclusive.", _month.toString())); + assert(() => _period <= lifeLimit, _t("The period (%s) must be less than or equal to %s.", _period.toString(), lifeLimit.toString())); + const monthPart = _month / 12; + let rate = 1 - Math.pow(_salvage / _cost, 1 / _life); + // round to 3 decimal places + rate = Math.round(rate * 1000) / 1000; + let before = _cost; + let after = _cost * (1 - rate * monthPart); + for (let i = 1; i < _period; i++) { + before = after; + after = before * (1 - rate); + if (i === _life) { + after = before * (1 - rate * (1 - monthPart)); + } + } + return before - after; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DDB + // ----------------------------------------------------------------------------- + const DEFAULT_DDB_DEPRECIATION_FACTOR = 2; + const DDB = { + description: _t("Depreciation via double-declining balance method."), + args: [ + arg("cost (number)", _t("The initial cost of the asset.")), + arg("salvage (number)", _t("The value of the asset at the end of depreciation.")), + arg("life (number)", _t("The number of periods over which the asset is depreciated.")), + arg("period (number)", _t("The single period within life for which to calculate depreciation.")), + arg(`factor (number, default=${DEFAULT_DDB_DEPRECIATION_FACTOR})`, _t("The factor by which depreciation decreases.")), + ], + returns: ["NUMBER"], + computeFormat: () => "#,##0.00", + compute: function (cost, salvage, life, period, factor = DEFAULT_DDB_DEPRECIATION_FACTOR) { + factor = factor || 0; + const _cost = toNumber(cost, this.locale); + const _salvage = toNumber(salvage, this.locale); + const _life = toNumber(life, this.locale); + const _period = toNumber(period, this.locale); + const _factor = toNumber(factor, this.locale); + assertCostPositiveOrZero(_cost); + assertSalvagePositiveOrZero(_salvage); + assertPeriodStrictlyPositive(_period); + assertLifeStrictlyPositive(_life); + assertPeriodSmallerOrEqualToLife(_period, _life); + assertDeprecationFactorStrictlyPositive(_factor); + if (_cost === 0 || _salvage >= _cost) + return 0; + const deprecFactor = _factor / _life; + if (deprecFactor > 1) { + return period === 1 ? _cost - _salvage : 0; + } + if (_period <= 1) { + return _cost * deprecFactor; + } + const previousCost = _cost * Math.pow(1 - deprecFactor, _period - 1); + const nextCost = _cost * Math.pow(1 - deprecFactor, _period); + const deprec = nextCost < _salvage ? previousCost - _salvage : previousCost - nextCost; + return Math.max(deprec, 0); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DISC + // ----------------------------------------------------------------------------- + const DISC = { + description: _t("Discount rate of a security based on price."), + args: [ + arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), + arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), + arg("price (number)", _t("The price at which the security is bought per 100 face value.")), + arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")), + arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), + ], + returns: ["NUMBER"], + compute: function (settlement, maturity, price, redemption, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const _settlement = Math.trunc(toNumber(settlement, this.locale)); + const _maturity = Math.trunc(toNumber(maturity, this.locale)); + const _price = toNumber(price, this.locale); + const _redemption = toNumber(redemption, this.locale); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); + assertDayCountConventionIsValid(_dayCountConvention); + assertPriceStrictlyPositive(_price); + assertRedemptionStrictlyPositive(_redemption); + /** + * https://support.microsoft.com/en-us/office/disc-function-71fce9f3-3f05-4acf-a5a3-eac6ef4daa53 + * + * B = number of days in year, depending on year basis + * DSM = number of days from settlement to maturity + * + * redemption - price B + * DISC = ____________________ * ____ + * redemption DSM + */ + const yearsFrac = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention); + return (_redemption - _price) / _redemption / yearsFrac; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DOLLARDE + // ----------------------------------------------------------------------------- + const DOLLARDE = { + description: _t("Convert a decimal fraction to decimal value."), + args: [ + arg("fractional_price (number)", _t("The price quotation given using fractional decimal conventions.")), + arg("unit (number)", _t("The units of the fraction, e.g. 8 for 1/8ths or 32 for 1/32nds.")), + ], + returns: ["NUMBER"], + compute: function (fractionalPrice, unit) { + const price = toNumber(fractionalPrice, this.locale); + const _unit = Math.trunc(toNumber(unit, this.locale)); + assert(() => _unit > 0, _t("The unit (%s) must be strictly positive.", _unit.toString())); + const truncatedPrice = Math.trunc(price); + const priceFractionalPart = price - truncatedPrice; + const frac = 10 ** Math.ceil(Math.log10(_unit)) / _unit; + return truncatedPrice + priceFractionalPart * frac; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DOLLARFR + // ----------------------------------------------------------------------------- + const DOLLARFR = { + description: _t("Convert a decimal value to decimal fraction."), + args: [ + arg("decimal_price (number)", _t("The price quotation given as a decimal value.")), + arg("unit (number)", _t("The units of the desired fraction, e.g. 8 for 1/8ths or 32 for 1/32nds.")), + ], + returns: ["NUMBER"], + compute: function (decimalPrice, unit) { + const price = toNumber(decimalPrice, this.locale); + const _unit = Math.trunc(toNumber(unit, this.locale)); + assert(() => _unit > 0, _t("The unit (%s) must be strictly positive.", _unit.toString())); + const truncatedPrice = Math.trunc(price); + const priceFractionalPart = price - truncatedPrice; + const frac = _unit / 10 ** Math.ceil(Math.log10(_unit)); + return truncatedPrice + priceFractionalPart * frac; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DURATION + // ----------------------------------------------------------------------------- + const DURATION = { + description: _t("Number of periods for an investment to reach a value."), + args: [ + arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), + arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), + arg("rate (number)", _t("The annualized rate of interest.")), + arg("yield (number)", _t("The expected annual yield of the security.")), + arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4).")), + arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), + ], + returns: ["NUMBER"], + compute: function (settlement, maturity, rate, securityYield, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const start = Math.trunc(toNumber(settlement, this.locale)); + const end = Math.trunc(toNumber(maturity, this.locale)); + const _rate = toNumber(rate, this.locale); + const _yield = toNumber(securityYield, this.locale); + const _frequency = Math.trunc(toNumber(frequency, this.locale)); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertMaturityAndSettlementDatesAreValid(start, end); + assertCouponFrequencyIsValid(_frequency); + assertDayCountConventionIsValid(_dayCountConvention); + assert(() => _rate >= 0, _t("The rate (%s) must be positive or null.", _rate.toString())); + assert(() => _yield >= 0, _t("The yield (%s) must be positive or null.", _yield.toString())); + const years = YEARFRAC.compute.bind(this)(start, end, _dayCountConvention); + const timeFirstYear = years - Math.trunc(years) || 1 / _frequency; + const nbrCoupons = Math.ceil(years * _frequency); + // The DURATION function return the Macaulay duration + // See example: https://en.wikipedia.org/wiki/Bond_duration#Formulas + const cashFlowFromCoupon = _rate / _frequency; + const yieldPerPeriod = _yield / _frequency; + let count = 0; + let sum = 0; + for (let i = 1; i <= nbrCoupons; i++) { + const cashFlowPerPeriod = cashFlowFromCoupon + (i === nbrCoupons ? 1 : 0); + const presentValuePerPeriod = cashFlowPerPeriod / (1 + yieldPerPeriod) ** i; + sum += (timeFirstYear + (i - 1) / _frequency) * presentValuePerPeriod; + count += presentValuePerPeriod; + } + return count === 0 ? 0 : sum / count; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // EFFECT + // ----------------------------------------------------------------------------- + const EFFECT = { + description: _t("Annual effective interest rate."), + args: [ + arg("nominal_rate (number)", _t("The nominal interest rate per year.")), + arg("periods_per_year (number)", _t("The number of compounding periods per year.")), + ], + returns: ["NUMBER"], + compute: function (nominal_rate, periods_per_year) { + const nominal = toNumber(nominal_rate, this.locale); + const periods = Math.trunc(toNumber(periods_per_year, this.locale)); + assert(() => nominal > 0, _t("The nominal rate (%s) must be strictly greater than 0.", nominal.toString())); + assert(() => periods > 0, _t("The number of periods by year (%s) must strictly greater than 0.", periods.toString())); + // https://en.wikipedia.org/wiki/Nominal_interest_rate#Nominal_versus_effective_interest_rate + return Math.pow(1 + nominal / periods, periods) - 1; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // FV + // ----------------------------------------------------------------------------- + const DEFAULT_PRESENT_VALUE = 0; + const FV = { + description: _t("Future value of an annuity investment."), + args: [ + arg("rate (number)", _t("The interest rate.")), + arg("number_of_periods (number)", _t("The number of payments to be made.")), + arg("payment_amount (number)", _t("The amount per period to be paid.")), + arg(`present_value (number, default=${DEFAULT_PRESENT_VALUE})`, _t("The current value of the annuity.")), + arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), + ], + returns: ["NUMBER"], + // to do: replace by dollar format + computeFormat: () => "#,##0.00", + compute: function (rate, numberOfPeriods, paymentAmount, presentValue = DEFAULT_PRESENT_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) { + presentValue = presentValue || 0; + endOrBeginning = endOrBeginning || 0; + const r = toNumber(rate, this.locale); + const n = toNumber(numberOfPeriods, this.locale); + const p = toNumber(paymentAmount, this.locale); + const pv = toNumber(presentValue, this.locale); + const type = toBoolean(endOrBeginning) ? 1 : 0; + return r ? -pv * (1 + r) ** n - (p * (1 + r * type) * ((1 + r) ** n - 1)) / r : -(pv + p * n); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // FVSCHEDULE + // ----------------------------------------------------------------------------- + const FVSCHEDULE = { + description: _t("Future value of principal from series of rates."), + args: [ + arg("principal (number)", _t("The amount of initial capital or value to compound against.")), + arg("rate_schedule (number, range)", _t("A series of interest rates to compound against the principal.")), + ], + returns: ["NUMBER"], + compute: function (principalAmount, rateSchedule) { + const principal = toNumber(principalAmount, this.locale); + return reduceAny([rateSchedule], (acc, rate) => acc * (1 + toNumber(rate, this.locale)), principal); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // INTRATE + // ----------------------------------------------------------------------------- + const INTRATE = { + description: _t("Calculates effective interest rate."), + args: [ + arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), + arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), + arg("investment (number)", _t("The amount invested in the security.")), + arg("redemption (number)", _t("The amount to be received at maturity.")), + arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), + ], + returns: ["NUMBER"], + compute: function (settlement, maturity, investment, redemption, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + const _settlement = Math.trunc(toNumber(settlement, this.locale)); + const _maturity = Math.trunc(toNumber(maturity, this.locale)); + const _redemption = toNumber(redemption, this.locale); + const _investment = toNumber(investment, this.locale); + assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); + assertInvestmentStrictlyPositive(_investment); + assertRedemptionStrictlyPositive(_redemption); + /** + * https://wiki.documentfoundation.org/Documentation/Calc_Functions/INTRATE + * + * (Redemption - Investment) / Investment + * INTRATE = _________________________________________ + * YEARFRAC(settlement, maturity, basis) + */ + const yearFrac = YEARFRAC.compute.bind(this)(_settlement, _maturity, dayCountConvention); + return (_redemption - _investment) / _investment / yearFrac; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // IPMT + // ----------------------------------------------------------------------------- + const IPMT = { + description: _t("Payment on the principal of an investment."), + args: [ + arg("rate (number)", _t("The annualized rate of interest.")), + arg("period (number)", _t("The amortization period, in terms of number of periods.")), + arg("number_of_periods (number)", _t("The number of payments to be made.")), + arg("present_value (number)", _t("The current value of the annuity.")), + arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")), + arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), + ], + returns: ["NUMBER"], + computeFormat: () => "#,##0.00", + compute: function (rate, currentPeriod, numberOfPeriods, presentValue, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) { + const payment = PMT.compute.bind(this)(rate, numberOfPeriods, presentValue, futureValue, endOrBeginning); + const ppmt = PPMT.compute.bind(this)(rate, currentPeriod, numberOfPeriods, presentValue, futureValue, endOrBeginning); + return payment - ppmt; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // IRR + // ----------------------------------------------------------------------------- + const DEFAULT_RATE_GUESS = 0.1; + const IRR = { + description: _t("Internal rate of return given periodic cashflows."), + args: [ + arg("cashflow_amounts (number, range)", _t("An array or range containing the income or payments associated with the investment.")), + arg(`rate_guess (number, default=${DEFAULT_RATE_GUESS})`, _t("An estimate for what the internal rate of return will be.")), + ], + returns: ["NUMBER"], + computeFormat: () => "0%", + compute: function (cashFlowAmounts, rateGuess = DEFAULT_RATE_GUESS) { + const _rateGuess = toNumber(rateGuess, this.locale); + assertRateGuessStrictlyGreaterThanMinusOne(_rateGuess); + // check that values contains at least one positive value and one negative value + // and extract number present in the cashFlowAmount argument + let positive = false; + let negative = false; + let amounts = []; + visitNumbers([cashFlowAmounts], (amount) => { + if (amount > 0) + positive = true; + if (amount < 0) + negative = true; + amounts.push(amount); + }, this.locale); + assert(() => positive && negative, _t("The cashflow_amounts must include negative and positive values.")); + const firstAmount = amounts.shift(); + // The result of IRR is the rate at which the NPV() function will return zero with the given values. + // This algorithm uses the Newton's method on the NPV function to determine the result + // Newton's method: https://en.wikipedia.org/wiki/Newton%27s_method + // As the NPV function isn't continuous, we apply the Newton's method on the numerator of the NPV formula. + function npvNumerator(rate, startValue, values) { + const nbrValue = values.length; + let i = 0; + return values.reduce((acc, v) => { + i++; + return acc + v * rate ** (nbrValue - i); + }, startValue * rate ** nbrValue); + } + function npvNumeratorDeriv(rate, startValue, values) { + const nbrValue = values.length; + let i = 0; + return values.reduce((acc, v) => { + i++; + return acc + v * (nbrValue - i) * rate ** (nbrValue - i - 1); + }, startValue * nbrValue * rate ** (nbrValue - 1)); + } + function func(x) { + return npvNumerator(x, firstAmount, amounts); + } + function derivFunc(x) { + return npvNumeratorDeriv(x, firstAmount, amounts); + } + return newtonMethod(func, derivFunc, _rateGuess + 1, 20, 1e-5) - 1; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ISPMT + // ----------------------------------------------------------------------------- + const ISPMT = { + description: _t("Returns the interest paid at a particular period of an investment."), + args: [ + arg("rate (number)", _t("The interest rate.")), + arg("period (number)", _t("The period for which you want to view the interest payment.")), + arg("number_of_periods (number)", _t("The number of payments to be made.")), + arg("present_value (number)", _t("The current value of the annuity.")), + ], + returns: ["NUMBER"], + compute: function (rate, currentPeriod, numberOfPeriods, presentValue) { + const interestRate = toNumber(rate, this.locale); + const period = toNumber(currentPeriod, this.locale); + const nOfPeriods = toNumber(numberOfPeriods, this.locale); + const investment = toNumber(presentValue, this.locale); + assert(() => nOfPeriods !== 0, _t("The number of periods must be different than 0.", nOfPeriods.toString())); + const currentInvestment = investment - investment * (period / nOfPeriods); + return -1 * currentInvestment * interestRate; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MDURATION + // ----------------------------------------------------------------------------- + const MDURATION = { + description: _t("Modified Macaulay duration."), + args: [ + arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), + arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), + arg("rate (number)", _t("The annualized rate of interest.")), + arg("yield (number)", _t("The expected annual yield of the security.")), + arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4).")), + arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), + ], + returns: ["NUMBER"], + compute: function (settlement, maturity, rate, securityYield, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + const duration = DURATION.compute.bind(this)(settlement, maturity, rate, securityYield, frequency, dayCountConvention); + const y = toNumber(securityYield, this.locale); + const k = Math.trunc(toNumber(frequency, this.locale)); + return duration / (1 + y / k); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MIRR + // ----------------------------------------------------------------------------- + const MIRR = { + description: _t("Modified internal rate of return."), + args: [ + arg("cashflow_amounts (range)", _t("A range containing the income or payments associated with the investment. The array should contain bot payments and incomes.")), + arg("financing_rate (number)", _t("The interest rate paid on funds invested.")), + arg("reinvestment_return_rate (number)", _t("The return (as a percentage) earned on reinvestment of income received from the investment.")), + ], + returns: ["NUMBER"], + compute: function (cashflowAmount, financingRate, reinvestmentRate) { + const fRate = toNumber(financingRate, this.locale); + const rRate = toNumber(reinvestmentRate, this.locale); + const cashFlow = transposeMatrix(cashflowAmount) + .flat() + .filter((t) => t !== null) + .map((val) => toNumber(val, this.locale)); + const n = cashFlow.length; + /** + * https://en.wikipedia.org/wiki/Modified_internal_rate_of_return + * + * / FV(positive cash flows, reinvestment rate) \ ^ (1 / (n - 1)) + * MIRR = | ___________________________________________ | - 1 + * \ - PV(negative cash flows, finance rate) / + * + * with n the number of cash flows. + * + * You can compute FV and PV as : + * + * FV = SUM [ (cashFlow[i]>0 ? cashFlow[i] : 0) * (1 + rRate)**(n - i-1) ] + * i= 0 => n + * + * PV = SUM [ (cashFlow[i]<0 ? cashFlow[i] : 0) / (1 + fRate)**i ] + * i= 0 => n + */ + let fv = 0; + let pv = 0; + for (const i of range(0, n)) { + const amount = cashFlow[i]; + if (amount >= 0) { + fv += amount * (rRate + 1) ** (n - i - 1); + } + else { + pv += amount / (fRate + 1) ** i; + } + } + assert(() => pv !== 0 && fv !== 0, _t("There must be both positive and negative values in cashflow_amounts.")); + const exponent = 1 / (n - 1); + return (-fv / pv) ** exponent - 1; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // NOMINAL + // ----------------------------------------------------------------------------- + const NOMINAL = { + description: _t("Annual nominal interest rate."), + args: [ + arg("effective_rate (number)", _t("The effective interest rate per year.")), + arg("periods_per_year (number)", _t("The number of compounding periods per year.")), + ], + returns: ["NUMBER"], + compute: function (effective_rate, periods_per_year) { + const effective = toNumber(effective_rate, this.locale); + const periods = Math.trunc(toNumber(periods_per_year, this.locale)); + assert(() => effective > 0, _t("The effective rate (%s) must must strictly greater than 0.", effective.toString())); + assert(() => periods > 0, _t("The number of periods by year (%s) must strictly greater than 0.", periods.toString())); + // https://en.wikipedia.org/wiki/Nominal_interest_rate#Nominal_versus_effective_interest_rate + return (Math.pow(effective + 1, 1 / periods) - 1) * periods; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // NPER + // ----------------------------------------------------------------------------- + const NPER = { + description: _t("Number of payment periods for an investment."), + args: [ + arg("rate (number)", _t("The interest rate.")), + arg("payment_amount (number)", _t("The amount of each payment made.")), + arg("present_value (number)", _t("The current value of the annuity.")), + arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")), + arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), + ], + returns: ["NUMBER"], + compute: function (rate, paymentAmount, presentValue, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) { + futureValue = futureValue || 0; + endOrBeginning = endOrBeginning || 0; + const r = toNumber(rate, this.locale); + const p = toNumber(paymentAmount, this.locale); + const pv = toNumber(presentValue, this.locale); + const fv = toNumber(futureValue, this.locale); + const t = toBoolean(endOrBeginning) ? 1 : 0; + /** + * https://wiki.documentfoundation.org/Documentation/Calc_Functions/NPER + * + * 0 = pv * (1 + r)^N + fv + [ p * (1 + r * t) * ((1 + r)^N - 1) ] / r + * + * We solve the equation for N: + * + * with C = [ p * (1 + r * t)] / r and + * R = 1 + r + * + * => 0 = pv * R^N + C * R^N - C + fv + * <=> (C - fv) = R^N * (pv + C) + * <=> log[(C - fv) / (pv + C)] = N * log(R) + */ + if (r === 0) { + return -(fv + pv) / p; + } + const c = (p * (1 + r * t)) / r; + return Math.log((c - fv) / (pv + c)) / Math.log(1 + r); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // NPV + // ----------------------------------------------------------------------------- + function npvResult(r, startValue, values, locale) { + let i = 0; + return reduceNumbers(values, (acc, v) => { + i++; + return acc + v / (1 + r) ** i; + }, startValue, locale); + } + const NPV = { + description: _t("The net present value of an investment based on a series of periodic cash flows and a discount rate."), + args: [ + arg("discount (number)", _t("The discount rate of the investment over one period.")), + arg("cashflow1 (number, range)", _t("The first future cash flow.")), + arg("cashflow2 (number, range, repeating)", _t("Additional future cash flows.")), + ], + returns: ["NUMBER"], + // to do: replace by dollar format + computeFormat: () => "#,##0.00", + compute: function (discount, ...values) { + const _discount = toNumber(discount, this.locale); + assert(() => _discount !== -1, _t("The discount (%s) must be different from -1.", _discount.toString())); + return npvResult(_discount, 0, values, this.locale); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // PDURATION + // ----------------------------------------------------------------------------- + const PDURATION = { + description: _t("Computes the number of periods needed for an investment to reach a value."), + args: [ + arg("rate (number)", _t("The rate at which the investment grows each period.")), + arg("present_value (number)", _t("The investment's current value.")), + arg("future_value (number)", _t("The investment's desired future value.")), + ], + returns: ["NUMBER"], + compute: function (rate, presentValue, futureValue) { + const _rate = toNumber(rate, this.locale); + const _presentValue = toNumber(presentValue, this.locale); + const _futureValue = toNumber(futureValue, this.locale); + assertRateStrictlyPositive(_rate); + assert(() => _presentValue > 0, _t("The present_value (%s) must be strictly positive.", _presentValue.toString())); + assert(() => _futureValue > 0, _t("The future_value (%s) must be strictly positive.", _futureValue.toString())); + return (Math.log(_futureValue) - Math.log(_presentValue)) / Math.log(1 + _rate); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // PMT + // ----------------------------------------------------------------------------- + const PMT = { + description: _t("Periodic payment for an annuity investment."), + args: [ + arg("rate (number)", _t("The annualized rate of interest.")), + arg("number_of_periods (number)", _t("The number of payments to be made.")), + arg("present_value (number)", _t("The current value of the annuity.")), + arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")), + arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), + ], + returns: ["NUMBER"], + computeFormat: () => "#,##0.00", + compute: function (rate, numberOfPeriods, presentValue, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) { + futureValue = futureValue || 0; + endOrBeginning = endOrBeginning || 0; + const n = toNumber(numberOfPeriods, this.locale); + const r = toNumber(rate, this.locale); + const t = toBoolean(endOrBeginning) ? 1 : 0; + let fv = toNumber(futureValue, this.locale); + let pv = toNumber(presentValue, this.locale); + assertNumberOfPeriodsStrictlyPositive(n); + /** + * https://wiki.documentfoundation.org/Documentation/Calc_Functions/PMT + * + * 0 = pv * (1 + r)^N + fv + [ p * (1 + r * t) * ((1 + r)^N - 1) ] / r + * + * We simply the equation for p + */ + if (r === 0) { + return -(fv + pv) / n; + } + let payment = -(pv * (1 + r) ** n + fv); + payment = (payment * r) / ((1 + r * t) * ((1 + r) ** n - 1)); + return payment; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // PPMT + // ----------------------------------------------------------------------------- + const PPMT = { + description: _t("Payment on the principal of an investment."), + args: [ + arg("rate (number)", _t("The annualized rate of interest.")), + arg("period (number)", _t("The amortization period, in terms of number of periods.")), + arg("number_of_periods (number)", _t("The number of payments to be made.")), + arg("present_value (number)", _t("The current value of the annuity.")), + arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")), + arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), + ], + returns: ["NUMBER"], + computeFormat: () => "#,##0.00", + compute: function (rate, currentPeriod, numberOfPeriods, presentValue, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) { + futureValue = futureValue || 0; + endOrBeginning = endOrBeginning || 0; + const n = toNumber(numberOfPeriods, this.locale); + const r = toNumber(rate, this.locale); + const period = toNumber(currentPeriod, this.locale); + const type = toBoolean(endOrBeginning) ? 1 : 0; + const fv = toNumber(futureValue, this.locale); + const pv = toNumber(presentValue, this.locale); + assertNumberOfPeriodsStrictlyPositive(n); + assert(() => period > 0 && period <= n, _t("The period must be between 1 and number_of_periods", n.toString())); + const payment = PMT.compute.bind(this)(r, n, pv, fv, endOrBeginning); + if (type === 1 && period === 1) + return payment; + const eqPeriod = type === 0 ? period - 1 : period - 2; + const eqPv = pv + payment * type; + const capitalAtPeriod = -FV.compute.bind(this)(r, eqPeriod, payment, eqPv, 0); + const currentInterest = capitalAtPeriod * r; + return payment + currentInterest; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // PV + // ----------------------------------------------------------------------------- + const PV = { + description: _t("Present value of an annuity investment."), + args: [ + arg("rate (number)", _t("The interest rate.")), + arg("number_of_periods (number)", _t("The number of payments to be made.")), + arg("payment_amount (number)", _t("The amount per period to be paid.")), + arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")), + arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), + ], + returns: ["NUMBER"], + // to do: replace by dollar format + computeFormat: () => "#,##0.00", + compute: function (rate, numberOfPeriods, paymentAmount, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) { + futureValue = futureValue || 0; + endOrBeginning = endOrBeginning || 0; + const r = toNumber(rate, this.locale); + const n = toNumber(numberOfPeriods, this.locale); + const p = toNumber(paymentAmount, this.locale); + const fv = toNumber(futureValue, this.locale); + const type = toBoolean(endOrBeginning) ? 1 : 0; + // https://wiki.documentfoundation.org/Documentation/Calc_Functions/PV + return r ? -((p * (1 + r * type) * ((1 + r) ** n - 1)) / r + fv) / (1 + r) ** n : -(fv + p * n); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // PRICE + // ----------------------------------------------------------------------------- + const PRICE = { + description: _t("Price of a security paying periodic interest."), + args: [ + arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), + arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), + arg("rate (number)", _t("The annualized rate of interest.")), + arg("yield (number)", _t("The expected annual yield of the security.")), + arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")), + arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4).")), + arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), + ], + returns: ["NUMBER"], + compute: function (settlement, maturity, rate, securityYield, redemption, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const _settlement = Math.trunc(toNumber(settlement, this.locale)); + const _maturity = Math.trunc(toNumber(maturity, this.locale)); + const _rate = toNumber(rate, this.locale); + const _yield = toNumber(securityYield, this.locale); + const _redemption = toNumber(redemption, this.locale); + const _frequency = Math.trunc(toNumber(frequency, this.locale)); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); + assertCouponFrequencyIsValid(_frequency); + assertDayCountConventionIsValid(_dayCountConvention); + assert(() => _rate >= 0, _t("The rate (%s) must be positive or null.", _rate.toString())); + assert(() => _yield >= 0, _t("The yield (%s) must be positive or null.", _yield.toString())); + assertRedemptionStrictlyPositive(_redemption); + const years = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention); + const nbrRealCoupons = years * _frequency; + const nbrFullCoupons = Math.ceil(nbrRealCoupons); + const timeFirstCoupon = nbrRealCoupons - Math.floor(nbrRealCoupons) || 1; + const yieldFactorPerPeriod = 1 + _yield / _frequency; + const cashFlowFromCoupon = (100 * _rate) / _frequency; + if (nbrFullCoupons === 1) { + return ((cashFlowFromCoupon + _redemption) / ((timeFirstCoupon * _yield) / _frequency + 1) - + cashFlowFromCoupon * (1 - timeFirstCoupon)); + } + let cashFlowsPresentValue = 0; + for (let i = 1; i <= nbrFullCoupons; i++) { + cashFlowsPresentValue += + cashFlowFromCoupon / yieldFactorPerPeriod ** (i - 1 + timeFirstCoupon); + } + const redemptionPresentValue = _redemption / yieldFactorPerPeriod ** (nbrFullCoupons - 1 + timeFirstCoupon); + return (redemptionPresentValue + cashFlowsPresentValue - cashFlowFromCoupon * (1 - timeFirstCoupon)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // PRICEDISC + // ----------------------------------------------------------------------------- + const PRICEDISC = { + description: _t("Price of a discount security."), + args: [ + arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), + arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), + arg("discount (number)", _t("The discount rate of the security at time of purchase.")), + arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")), + arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), + ], + returns: ["NUMBER"], + compute: function (settlement, maturity, discount, redemption, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const _settlement = Math.trunc(toNumber(settlement, this.locale)); + const _maturity = Math.trunc(toNumber(maturity, this.locale)); + const _discount = toNumber(discount, this.locale); + const _redemption = toNumber(redemption, this.locale); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); + assertDayCountConventionIsValid(_dayCountConvention); + assertDiscountStrictlyPositive(_discount); + assertRedemptionStrictlyPositive(_redemption); + /** + * https://support.microsoft.com/en-us/office/pricedisc-function-d06ad7c1-380e-4be7-9fd9-75e3079acfd3 + * + * B = number of days in year, depending on year basis + * DSM = number of days from settlement to maturity + * + * PRICEDISC = redemption - discount * redemption * (DSM/B) + */ + const yearsFrac = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention); + return _redemption - _discount * _redemption * yearsFrac; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // PRICEMAT + // ----------------------------------------------------------------------------- + const PRICEMAT = { + description: _t("Calculates the price of a security paying interest at maturity, based on expected yield."), + args: [ + arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), + arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), + arg("issue (date)", _t("The date the security was initially issued.")), + arg("rate (number)", _t("The annualized rate of interest.")), + arg("yield (number)", _t("The expected annual yield of the security.")), + arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), + ], + returns: ["NUMBER"], + compute: function (settlement, maturity, issue, rate, securityYield, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const _settlement = Math.trunc(toNumber(settlement, this.locale)); + const _maturity = Math.trunc(toNumber(maturity, this.locale)); + const _issue = Math.trunc(toNumber(issue, this.locale)); + const _rate = toNumber(rate, this.locale); + const _yield = toNumber(securityYield, this.locale); + const _dayCount = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertSettlementAndIssueDatesAreValid(_settlement, _issue); + assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); + assertDayCountConventionIsValid(_dayCount); + assert(() => _rate >= 0, _t("The rate (%s) must be positive or null.", _rate.toString())); + assert(() => _yield >= 0, _t("The yield (%s) must be positive or null.", _yield.toString())); + /** + * https://support.microsoft.com/en-us/office/pricemat-function-52c3b4da-bc7e-476a-989f-a95f675cae77 + * + * B = number of days in year, depending on year basis + * DSM = number of days from settlement to maturity + * DIM = number of days from issue to maturity + * DIS = number of days from issue to settlement + * + * 100 + (DIM/B * rate * 100) + * PRICEMAT = __________________________ - (DIS/B * rate * 100) + * 1 + (DSM/B * yield) + * + * The ratios number_of_days / days_in_year are computed using the YEARFRAC function, that handle + * differences due to day count conventions. + * + * Compatibility note : + * + * Contrary to GSheet and OpenOffice, Excel doesn't seems to always use its own YEARFRAC function + * to compute PRICEMAT, and give different values for some combinations of dates and day count + * conventions ( notably for leap years and dayCountConvention = 1 (Actual/Actual)). + * + * Our function PRICEMAT give us the same results as LibreOffice Calc. + * Google Sheet use the formula with YEARFRAC, but its YEARFRAC function results are different + * from the results of Excel/LibreOffice, thus we get different values with PRICEMAT. + * + */ + const settlementToMaturity = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCount); + const issueToSettlement = YEARFRAC.compute.bind(this)(_settlement, _issue, _dayCount); + const issueToMaturity = YEARFRAC.compute.bind(this)(_issue, _maturity, _dayCount); + const numerator = 100 + issueToMaturity * _rate * 100; + const denominator = 1 + settlementToMaturity * _yield; + const term2 = issueToSettlement * _rate * 100; + return numerator / denominator - term2; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // RATE + // ----------------------------------------------------------------------------- + const RATE_GUESS_DEFAULT = 0.1; + const RATE = { + description: _t("Interest rate of an annuity investment."), + args: [ + arg("number_of_periods (number)", _t("The number of payments to be made.")), + arg("payment_per_period (number)", _t("The amount per period to be paid.")), + arg("present_value (number)", _t("The current value of the annuity.")), + arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")), + arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")), + arg(`rate_guess (number, default=${RATE_GUESS_DEFAULT})`, _t("An estimate for what the interest rate will be.")), + ], + returns: ["NUMBER"], + computeFormat: () => "0%", + compute: function (numberOfPeriods, paymentPerPeriod, presentValue, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING, rateGuess = RATE_GUESS_DEFAULT) { + futureValue = futureValue || 0; + endOrBeginning = endOrBeginning || 0; + rateGuess = rateGuess || RATE_GUESS_DEFAULT; + const n = toNumber(numberOfPeriods, this.locale); + const payment = toNumber(paymentPerPeriod, this.locale); + const type = toBoolean(endOrBeginning) ? 1 : 0; + const guess = toNumber(rateGuess, this.locale); + let fv = toNumber(futureValue, this.locale); + let pv = toNumber(presentValue, this.locale); + assertNumberOfPeriodsStrictlyPositive(n); + assert(() => [payment, pv, fv].some((val) => val > 0) && [payment, pv, fv].some((val) => val < 0), _t("There must be both positive and negative values in [payment_amount, present_value, future_value].", n.toString())); + assertRateGuessStrictlyGreaterThanMinusOne(guess); + fv -= payment * type; + pv += payment * type; + // https://github.com/apache/openoffice/blob/trunk/main/sc/source/core/tool/interpr2.cxx + const func = (rate) => { + const powN = Math.pow(1 + rate, n); + const intResult = (powN - 1) / rate; + return fv + pv * powN + payment * intResult; + }; + const derivFunc = (rate) => { + const powNMinus1 = Math.pow(1 + rate, n - 1); + const powN = Math.pow(1 + rate, n); + const intResult = (powN - 1) / rate; + const intResultDeriv = (n * powNMinus1) / rate - intResult / rate; + const fTermDerivation = pv * n * powNMinus1 + payment * intResultDeriv; + return fTermDerivation; + }; + return newtonMethod(func, derivFunc, guess, 40, 1e-5); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // RECEIVED + // ----------------------------------------------------------------------------- + const RECEIVED = { + description: _t("Amount received at maturity for a security."), + args: [ + arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), + arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), + arg("investment (number)", _t("The amount invested (irrespective of face value of each security).")), + arg("discount (number)", _t("The discount rate of the security invested in.")), + arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), + ], + returns: ["NUMBER"], + compute: function (settlement, maturity, investment, discount, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const _settlement = Math.trunc(toNumber(settlement, this.locale)); + const _maturity = Math.trunc(toNumber(maturity, this.locale)); + const _investment = toNumber(investment, this.locale); + const _discount = toNumber(discount, this.locale); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); + assertDayCountConventionIsValid(_dayCountConvention); + assertInvestmentStrictlyPositive(_investment); + assertDiscountStrictlyPositive(_discount); + /** + * https://support.microsoft.com/en-us/office/received-function-7a3f8b93-6611-4f81-8576-828312c9b5e5 + * + * investment + * RECEIVED = _________________________ + * 1 - discount * DSM / B + * + * with DSM = number of days from settlement to maturity and B = number of days in a year + * + * The ratio DSM/B can be computed with the YEARFRAC function to take the dayCountConvention into account. + */ + const yearsFrac = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention); + return _investment / (1 - _discount * yearsFrac); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // RRI + // ----------------------------------------------------------------------------- + const RRI = { + description: _t("Computes the rate needed for an investment to reach a specific value within a specific number of periods."), + args: [ + arg("number_of_periods (number)", _t("The number of periods.")), + arg("present_value (number)", _t("The present value of the investment.")), + arg("future_value (number)", _t("The future value of the investment.")), + ], + returns: ["NUMBER"], + compute: function (numberOfPeriods, presentValue, futureValue) { + const n = toNumber(numberOfPeriods, this.locale); + const pv = toNumber(presentValue, this.locale); + const fv = toNumber(futureValue, this.locale); + assertNumberOfPeriodsStrictlyPositive(n); + /** + * https://support.microsoft.com/en-us/office/rri-function-6f5822d8-7ef1-4233-944c-79e8172930f4 + * + * RRI = (future value / present value) ^ (1 / number of periods) - 1 + */ + return (fv / pv) ** (1 / n) - 1; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SLN + // ----------------------------------------------------------------------------- + const SLN = { + description: _t("Depreciation of an asset using the straight-line method."), + args: [ + arg("cost (number)", _t("The initial cost of the asset.")), + arg("salvage (number)", _t("The value of the asset at the end of depreciation.")), + arg("life (number)", _t("The number of periods over which the asset is depreciated.")), + ], + returns: ["NUMBER"], + computeFormat: () => "#,##0.00", + compute: function (cost, salvage, life) { + const _cost = toNumber(cost, this.locale); + const _salvage = toNumber(salvage, this.locale); + const _life = toNumber(life, this.locale); + // No assertion is done on the values of the arguments to be compatible with Excel/Gsheet that don't check the values. + // It's up to the user to make sure the arguments make sense, which is good design because the user is smart. + return (_cost - _salvage) / _life; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SYD + // ----------------------------------------------------------------------------- + const SYD = { + description: _t("Depreciation via sum of years digit method."), + args: [ + arg("cost (number)", _t("The initial cost of the asset.")), + arg("salvage (number)", _t("The value of the asset at the end of depreciation.")), + arg("life (number)", _t("The number of periods over which the asset is depreciated.")), + arg("period (number)", _t("The single period within life for which to calculate depreciation.")), + ], + returns: ["NUMBER"], + computeFormat: () => "#,##0.00", + compute: function (cost, salvage, life, period) { + const _cost = toNumber(cost, this.locale); + const _salvage = toNumber(salvage, this.locale); + const _life = toNumber(life, this.locale); + const _period = toNumber(period, this.locale); + assertPeriodStrictlyPositive(_period); + assertLifeStrictlyPositive(_life); + assertPeriodSmallerOrEqualToLife(_period, _life); + /** + * This deprecation method use the sum of digits of the periods of the life as the deprecation factor. + * For example for a life = 5, we have a deprecation factor or 1 + 2 + 3 + 4 + 5 = 15 = life * (life + 1) / 2 = F. + * + * The deprecation for a period p is then computed based on F and the remaining lifetime at the period P. + * + * deprecation = (cost - salvage) * (number of remaining periods / F) + */ + const deprecFactor = (_life * (_life + 1)) / 2; + const remainingPeriods = _life - _period + 1; + return (_cost - _salvage) * (remainingPeriods / deprecFactor); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TBILLPRICE + // ----------------------------------------------------------------------------- + const TBILLPRICE = { + description: _t("Price of a US Treasury bill."), + args: [ + arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), + arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), + arg("discount (number)", _t("The discount rate of the bill at time of purchase.")), + ], + returns: ["NUMBER"], + compute: function (settlement, maturity, discount) { + const start = Math.trunc(toNumber(settlement, this.locale)); + const end = Math.trunc(toNumber(maturity, this.locale)); + const disc = toNumber(discount, this.locale); + assertMaturityAndSettlementDatesAreValid(start, end); + assertSettlementLessThanOneYearBeforeMaturity(start, end, this.locale); + assertDiscountStrictlyPositive(disc); + assertDiscountStrictlySmallerThanOne(disc); + /** + * https://support.microsoft.com/en-us/office/tbillprice-function-eacca992-c29d-425a-9eb8-0513fe6035a2 + * + * TBILLPRICE = 100 * (1 - discount * DSM / 360) + * + * with DSM = number of days from settlement to maturity + * + * The ratio DSM/360 can be computed with the YEARFRAC function with dayCountConvention = 2 (actual/360). + */ + const yearFrac = YEARFRAC.compute.bind(this)(start, end, 2); + return 100 * (1 - disc * yearFrac); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TBILLEQ + // ----------------------------------------------------------------------------- + const TBILLEQ = { + description: _t("Equivalent rate of return for a US Treasury bill."), + args: [ + arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), + arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), + arg("discount (number)", _t("The discount rate of the bill at time of purchase.")), + ], + returns: ["NUMBER"], + compute: function (settlement, maturity, discount) { + const start = Math.trunc(toNumber(settlement, this.locale)); + const end = Math.trunc(toNumber(maturity, this.locale)); + const disc = toNumber(discount, this.locale); + assertMaturityAndSettlementDatesAreValid(start, end); + assertSettlementLessThanOneYearBeforeMaturity(start, end, this.locale); + assertDiscountStrictlyPositive(disc); + assertDiscountStrictlySmallerThanOne(disc); + /** + * https://support.microsoft.com/en-us/office/tbilleq-function-2ab72d90-9b4d-4efe-9fc2-0f81f2c19c8c + * + * 365 * discount + * TBILLEQ = ________________________ + * 360 - discount * DSM + * + * with DSM = number of days from settlement to maturity + * + * What is not indicated in the Excel documentation is that this formula only works for duration between settlement + * and maturity that are less than 6 months (182 days). This is because US Treasury bills use semi-annual interest, + * and thus we have to take into account the compound interest for the calculation. + * + * For this case, the formula becomes (Treasury Securities and Derivatives, by Frank J. Fabozzi, page 49) + * + * -2X + 2* SQRT[ X² - (2X - 1) * (1 - 100/p) ] + * TBILLEQ = ________________________________________________ + * 2X - 1 + * + * with X = DSM / (number of days in a year), + * and p is the price, computed with TBILLPRICE + * + * Note that from my tests in Excel, we take (number of days in a year) = 366 ONLY if DSM is 366, not if + * the settlement year is a leap year. + * + */ + const nDays = DAYS.compute.bind(this)(end, start); + if (nDays <= 182) { + return (365 * disc) / (360 - disc * nDays); + } + const p = TBILLPRICE.compute.bind(this)(start, end, disc) / 100; + const daysInYear = nDays === 366 ? 366 : 365; + const x = nDays / daysInYear; + const num = -2 * x + 2 * Math.sqrt(x ** 2 - (2 * x - 1) * (1 - 1 / p)); + const denom = 2 * x - 1; + return num / denom; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TBILLYIELD + // ----------------------------------------------------------------------------- + const TBILLYIELD = { + description: _t("The yield of a US Treasury bill based on price."), + args: [ + arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), + arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), + arg("price (number)", _t("The price at which the security is bought per 100 face value.")), + ], + returns: ["NUMBER"], + compute: function (settlement, maturity, price) { + const start = Math.trunc(toNumber(settlement, this.locale)); + const end = Math.trunc(toNumber(maturity, this.locale)); + const p = toNumber(price, this.locale); + assertMaturityAndSettlementDatesAreValid(start, end); + assertSettlementLessThanOneYearBeforeMaturity(start, end, this.locale); + assertPriceStrictlyPositive(p); + /** + * https://support.microsoft.com/en-us/office/tbillyield-function-6d381232-f4b0-4cd5-8e97-45b9c03468ba + * + * 100 - price 360 + * TBILLYIELD = ____________ * _____ + * price DSM + * + * with DSM = number of days from settlement to maturity + * + * The ratio DSM/360 can be computed with the YEARFRAC function with dayCountConvention = 2 (actual/360). + * + */ + const yearFrac = YEARFRAC.compute.bind(this)(start, end, 2); + return ((100 - p) / p) * (1 / yearFrac); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // VDB + // ----------------------------------------------------------------------------- + const DEFAULT_VDB_NO_SWITCH = false; + const VDB = { + description: _t("Variable declining balance. WARNING : does not handle decimal periods."), + args: [ + arg("cost (number)", _t("The initial cost of the asset.")), + arg("salvage (number)", _t("The value of the asset at the end of depreciation.")), + arg("life (number)", _t("The number of periods over which the asset is depreciated.")), + arg("start (number)", _t("Starting period to calculate depreciation.")), + arg("end (number)", _t("Ending period to calculate depreciation.")), + arg(`factor (number, default=${DEFAULT_DDB_DEPRECIATION_FACTOR})`, _t("The number of months in the first year of depreciation.")), + arg(`no_switch (number, default=${DEFAULT_VDB_NO_SWITCH})`, _t("Whether to switch to straight-line depreciation when the depreciation is greater than the declining balance calculation.")), + ], + returns: ["NUMBER"], + compute: function (cost, salvage, life, startPeriod, endPeriod, factor = DEFAULT_DDB_DEPRECIATION_FACTOR, noSwitch = DEFAULT_VDB_NO_SWITCH) { + factor = factor || 0; + const _cost = toNumber(cost, this.locale); + const _salvage = toNumber(salvage, this.locale); + const _life = toNumber(life, this.locale); + /* TODO : handle decimal periods + * on end_period it looks like it is a simple linear function, but I cannot understand exactly how + * decimals periods are handled with start_period. + */ + const _startPeriod = Math.trunc(toNumber(startPeriod, this.locale)); + const _endPeriod = Math.trunc(toNumber(endPeriod, this.locale)); + const _factor = toNumber(factor, this.locale); + const _noSwitch = toBoolean(noSwitch); + assertCostPositiveOrZero(_cost); + assertSalvagePositiveOrZero(_salvage); + assertStartAndEndPeriodAreValid(_startPeriod, _endPeriod, _life); + assertDeprecationFactorStrictlyPositive(_factor); + if (_cost === 0) + return 0; + if (_salvage >= _cost) { + return _startPeriod < 1 ? _cost - _salvage : 0; + } + const doubleDeprecFactor = _factor / _life; + if (doubleDeprecFactor >= 1) { + return _startPeriod < 1 ? _cost - _salvage : 0; + } + let previousCost = _cost; + let currentDeprec = 0; + let resultDeprec = 0; + let isLinearDeprec = false; + for (let i = 0; i < _endPeriod; i++) { + // compute the current deprecation, or keep the last one if we reached a stage of linear deprecation + if (!isLinearDeprec || _noSwitch) { + const doubleDeprec = previousCost * doubleDeprecFactor; + const remainingPeriods = _life - i; + const linearDeprec = (previousCost - _salvage) / remainingPeriods; + if (!_noSwitch && linearDeprec > doubleDeprec) { + isLinearDeprec = true; + currentDeprec = linearDeprec; + } + else { + currentDeprec = doubleDeprec; + } + } + const nextCost = Math.max(previousCost - currentDeprec, _salvage); + if (i >= _startPeriod) { + resultDeprec += previousCost - nextCost; + } + previousCost = nextCost; + } + return resultDeprec; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // XIRR + // ----------------------------------------------------------------------------- + const XIRR = { + description: _t("Internal rate of return given non-periodic cash flows."), + args: [ + arg("cashflow_amounts (range)", _t("An range containing the income or payments associated with the investment.")), + arg("cashflow_dates (range)", _t("An range with dates corresponding to the cash flows in cashflow_amounts.")), + arg(`rate_guess (number, default=${RATE_GUESS_DEFAULT})`, _t("An estimate for what the internal rate of return will be.")), + ], + returns: ["NUMBER"], + compute: function (cashflowAmounts, cashflowDates, rateGuess = RATE_GUESS_DEFAULT) { + rateGuess = rateGuess || 0; + const guess = toNumber(rateGuess, this.locale); + const _cashFlows = cashflowAmounts.flat().map((val) => toNumber(val, this.locale)); + const _dates = cashflowDates.flat().map((val) => toNumber(val, this.locale)); + assertCashFlowsAndDatesHaveSameDimension(cashflowAmounts, cashflowDates); + assertCashFlowsHavePositiveAndNegativesValues(_cashFlows); + assertEveryDateGreaterThanFirstDateOfCashFlowDates(_dates); + assertRateGuessStrictlyGreaterThanMinusOne(guess); + const map = new Map(); + for (const i of range(0, _dates.length)) { + const date = _dates[i]; + if (map.has(date)) + map.set(date, map.get(date) + _cashFlows[i]); + else + map.set(date, _cashFlows[i]); + } + const dates = Array.from(map.keys()); + const values = dates.map((date) => map.get(date)); + /** + * https://support.microsoft.com/en-us/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d + * + * The rate is computed iteratively by trying to solve the equation + * + * + * 0 = SUM [ P_i * (1 + rate) ^((d_0 - d_i) / 365) ] + P_0 + * i = 1 => n + * + * with P_i = price number i + * d_i = date number i + * + * This function is not defined for rate < -1. For the case where we get rates < -1 in the Newton method, add + * a fallback for a number very close to -1 to continue the Newton method. + * + */ + const func = (rate) => { + let value = values[0]; + for (const i of range(1, values.length)) { + const dateDiff = (dates[0] - dates[i]) / 365; + value += values[i] * (1 + rate) ** dateDiff; + } + return value; + }; + const derivFunc = (rate) => { + let deriv = 0; + for (const i of range(1, values.length)) { + const dateDiff = (dates[0] - dates[i]) / 365; + deriv += dateDiff * values[i] * (1 + rate) ** (dateDiff - 1); + } + return deriv; + }; + const nanFallback = (previousFallback) => { + // -0.9 => -0.99 => -0.999 => ... + if (!previousFallback) + return -0.9; + return previousFallback / 10 - 0.9; + }; + return newtonMethod(func, derivFunc, guess, 40, 1e-5, nanFallback); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // XNPV + // ----------------------------------------------------------------------------- + const XNPV = { + description: _t("Net present value given to non-periodic cash flows.."), + args: [ + arg("discount (number)", _t("The discount rate of the investment over one period.")), + arg("cashflow_amounts (number, range)", _t("An range containing the income or payments associated with the investment.")), + arg("cashflow_dates (number, range)", _t("An range with dates corresponding to the cash flows in cashflow_amounts.")), + ], + returns: ["NUMBER"], + compute: function (discount, cashflowAmounts, cashflowDates) { + const rate = toNumber(discount, this.locale); + const _cashFlows = isMatrix(cashflowAmounts) + ? cashflowAmounts.flat().map((val) => strictToNumber(val, this.locale)) + : [strictToNumber(cashflowAmounts, this.locale)]; + const _dates = isMatrix(cashflowDates) + ? cashflowDates.flat().map((val) => strictToNumber(val, this.locale)) + : [strictToNumber(cashflowDates, this.locale)]; + if (isMatrix(cashflowDates) && isMatrix(cashflowAmounts)) { + assertCashFlowsAndDatesHaveSameDimension(cashflowAmounts, cashflowDates); + } + else { + assert(() => _cashFlows.length === _dates.length, _t("There must be the same number of values in cashflow_amounts and cashflow_dates.")); + } + assertEveryDateGreaterThanFirstDateOfCashFlowDates(_dates); + assertRateStrictlyPositive(rate); + if (_cashFlows.length === 1) + return _cashFlows[0]; + // aggregate values of the same date + const map = new Map(); + for (const i of range(0, _dates.length)) { + const date = _dates[i]; + if (map.has(date)) + map.set(date, map.get(date) + _cashFlows[i]); + else + map.set(date, _cashFlows[i]); + } + const dates = Array.from(map.keys()); + const values = dates.map((date) => map.get(date)); + /** + * https://support.microsoft.com/en-us/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d + * + * The present value is computed using + * + * + * NPV = SUM [ P_i *(1 + rate) ^((d_0 - d_i) / 365) ] + P_0 + * i = 1 => n + * + * with P_i = price number i + * d_i = date number i + * + * + */ + let pv = values[0]; + for (const i of range(1, values.length)) { + const dateDiff = (dates[0] - dates[i]) / 365; + pv += values[i] * (1 + rate) ** dateDiff; + } + return pv; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // YIELD + // ----------------------------------------------------------------------------- + const YIELD = { + description: _t("Annual yield of a security paying periodic interest."), + args: [ + arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), + arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), + arg("rate (number)", _t("The annualized rate of interest.")), + arg("price (number)", _t("The price at which the security is bought per 100 face value.")), + arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")), + arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4).")), + arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), + ], + returns: ["NUMBER"], + compute: function (settlement, maturity, rate, price, redemption, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const _settlement = Math.trunc(toNumber(settlement, this.locale)); + const _maturity = Math.trunc(toNumber(maturity, this.locale)); + const _rate = toNumber(rate, this.locale); + const _price = toNumber(price, this.locale); + const _redemption = toNumber(redemption, this.locale); + const _frequency = Math.trunc(toNumber(frequency, this.locale)); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); + assertCouponFrequencyIsValid(_frequency); + assertDayCountConventionIsValid(_dayCountConvention); + assert(() => _rate >= 0, _t("The rate (%s) must be positive or null.", _rate.toString())); + assertPriceStrictlyPositive(_price); + assertRedemptionStrictlyPositive(_redemption); + const years = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention); + const nbrRealCoupons = years * _frequency; + const nbrFullCoupons = Math.ceil(nbrRealCoupons); + const timeFirstCoupon = nbrRealCoupons - Math.floor(nbrRealCoupons) || 1; + const cashFlowFromCoupon = (100 * _rate) / _frequency; + if (nbrFullCoupons === 1) { + const subPart = _price + cashFlowFromCoupon * (1 - timeFirstCoupon); + return (((_redemption + cashFlowFromCoupon - subPart) * _frequency * (1 / timeFirstCoupon)) / + subPart); + } + // The result of YIELD function is the yield at which the PRICE function will return the given price. + // This algorithm uses the Newton's method on the PRICE function to determine the result. + // Newton's method: https://en.wikipedia.org/wiki/Newton%27s_method + // As the PRICE function isn't continuous, we apply the Newton's method on the numerator of the PRICE formula. + // For simplicity, it is not yield but yieldFactorPerPeriod (= 1 + yield / frequency) which will be calibrated in Newton's method. + // yield can be deduced from yieldFactorPerPeriod in sequence. + function priceNumerator(price, timeFirstCoupon, nbrFullCoupons, yieldFactorPerPeriod, cashFlowFromCoupon, redemption) { + let result = redemption - + (price + cashFlowFromCoupon * (1 - timeFirstCoupon)) * + yieldFactorPerPeriod ** (nbrFullCoupons - 1 + timeFirstCoupon); + for (let i = 1; i <= nbrFullCoupons; i++) { + result += cashFlowFromCoupon * yieldFactorPerPeriod ** (i - 1); + } + return result; + } + function priceNumeratorDeriv(price, timeFirstCoupon, nbrFullCoupons, yieldFactorPerPeriod, cashFlowFromCoupon) { + let result = -(price + cashFlowFromCoupon * (1 - timeFirstCoupon)) * + (nbrFullCoupons - 1 + timeFirstCoupon) * + yieldFactorPerPeriod ** (nbrFullCoupons - 2 + timeFirstCoupon); + for (let i = 1; i <= nbrFullCoupons; i++) { + result += cashFlowFromCoupon * (i - 1) * yieldFactorPerPeriod ** (i - 2); + } + return result; + } + function func(x) { + return priceNumerator(_price, timeFirstCoupon, nbrFullCoupons, x, cashFlowFromCoupon, _redemption); + } + function derivFunc(x) { + return priceNumeratorDeriv(_price, timeFirstCoupon, nbrFullCoupons, x, cashFlowFromCoupon); + } + const initYield = _rate + 1; + const initYieldFactorPerPeriod = 1 + initYield / _frequency; + const methodResult = newtonMethod(func, derivFunc, initYieldFactorPerPeriod, 100, 1e-5); + return (methodResult - 1) * _frequency; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // YIELDDISC + // ----------------------------------------------------------------------------- + const YIELDDISC = { + description: _t("Annual yield of a discount security."), + args: [ + arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), + arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), + arg("price (number)", _t("The price at which the security is bought per 100 face value.")), + arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")), + arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), + ], + returns: ["NUMBER"], + compute: function (settlement, maturity, price, redemption, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const _settlement = Math.trunc(toNumber(settlement, this.locale)); + const _maturity = Math.trunc(toNumber(maturity, this.locale)); + const _price = toNumber(price, this.locale); + const _redemption = toNumber(redemption, this.locale); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); + assertDayCountConventionIsValid(_dayCountConvention); + assertPriceStrictlyPositive(_price); + assertRedemptionStrictlyPositive(_redemption); + /** + * https://wiki.documentfoundation.org/Documentation/Calc_Functions/YIELDDISC + * + * (redemption / price) - 1 + * YIELDDISC = _____________________________________ + * YEARFRAC(settlement, maturity, basis) + */ + const yearFrac = YEARFRAC.compute.bind(this)(settlement, maturity, dayCountConvention); + return (_redemption / _price - 1) / yearFrac; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // YIELDMAT + // ----------------------------------------------------------------------------- + const YIELDMAT = { + description: _t("Annual yield of a security paying interest at maturity."), + args: [ + arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")), + arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")), + arg("issue (date)", _t("The date the security was initially issued.")), + arg("rate (number)", _t("The annualized rate of interest.")), + arg("price (number)", _t("The price at which the security is bought.")), + arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")), + ], + returns: ["NUMBER"], + compute: function (settlement, maturity, issue, rate, price, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) { + dayCountConvention = dayCountConvention || 0; + const _settlement = Math.trunc(toNumber(settlement, this.locale)); + const _maturity = Math.trunc(toNumber(maturity, this.locale)); + const _issue = Math.trunc(toNumber(issue, this.locale)); + const _rate = toNumber(rate, this.locale); + const _price = toNumber(price, this.locale); + const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale)); + assertMaturityAndSettlementDatesAreValid(_settlement, _maturity); + assertDayCountConventionIsValid(_dayCountConvention); + assert(() => _settlement >= _issue, _t("The settlement (%s) must be greater than or equal to the issue (%s).", _settlement.toString(), _issue.toString())); + assert(() => _rate >= 0, _t("The rate (%s) must be positive or null.", _rate.toString())); + assertPriceStrictlyPositive(_price); + const issueToMaturity = YEARFRAC.compute.bind(this)(_issue, _maturity, _dayCountConvention); + const issueToSettlement = YEARFRAC.compute.bind(this)(_issue, _settlement, _dayCountConvention); + const settlementToMaturity = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention); + const numerator = (100 * (1 + _rate * issueToMaturity)) / (_price + 100 * _rate * issueToSettlement) - 1; + return numerator / settlementToMaturity; + }, + isExported: true, + }; + + var financial = /*#__PURE__*/Object.freeze({ + __proto__: null, + ACCRINTM: ACCRINTM, + AMORLINC: AMORLINC, + COUPDAYBS: COUPDAYBS, + COUPDAYS: COUPDAYS, + COUPDAYSNC: COUPDAYSNC, + COUPNCD: COUPNCD, + COUPNUM: COUPNUM, + COUPPCD: COUPPCD, + CUMIPMT: CUMIPMT, + CUMPRINC: CUMPRINC, + DB: DB, + DDB: DDB, + DISC: DISC, + DOLLARDE: DOLLARDE, + DOLLARFR: DOLLARFR, + DURATION: DURATION, + EFFECT: EFFECT, + FV: FV, + FVSCHEDULE: FVSCHEDULE, + INTRATE: INTRATE, + IPMT: IPMT, + IRR: IRR, + ISPMT: ISPMT, + MDURATION: MDURATION, + MIRR: MIRR, + NOMINAL: NOMINAL, + NPER: NPER, + NPV: NPV, + PDURATION: PDURATION, + PMT: PMT, + PPMT: PPMT, + PRICE: PRICE, + PRICEDISC: PRICEDISC, + PRICEMAT: PRICEMAT, + PV: PV, + RATE: RATE, + RECEIVED: RECEIVED, + RRI: RRI, + SLN: SLN, + SYD: SYD, + TBILLEQ: TBILLEQ, + TBILLPRICE: TBILLPRICE, + TBILLYIELD: TBILLYIELD, + VDB: VDB, + XIRR: XIRR, + XNPV: XNPV, + YIELD: YIELD, + YIELDDISC: YIELDDISC, + YIELDMAT: YIELDMAT + }); + + // ----------------------------------------------------------------------------- + // ISERR + // ----------------------------------------------------------------------------- + const ISERR = { + description: _t("Whether a value is an error other than #N/A."), + args: [arg("value (any, lazy)", _t("The value to be verified as an error type."))], + returns: ["BOOLEAN"], + compute: function (value) { + try { + value(); + return false; + } + catch (e) { + return e?.errorType != CellErrorType.NotAvailable; + } + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ISERROR + // ----------------------------------------------------------------------------- + const ISERROR = { + description: _t("Whether a value is an error."), + args: [arg("value (any, lazy)", _t("The value to be verified as an error type."))], + returns: ["BOOLEAN"], + compute: function (value) { + try { + value(); + return false; + } + catch (e) { + return true; + } + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ISLOGICAL + // ----------------------------------------------------------------------------- + const ISLOGICAL = { + description: _t("Whether a value is `true` or `false`."), + args: [arg("value (any, lazy)", _t("The value to be verified as a logical TRUE or FALSE."))], + returns: ["BOOLEAN"], + compute: function (value) { + try { + return typeof value() === "boolean"; + } + catch (e) { + return false; + } + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ISNA + // ----------------------------------------------------------------------------- + const ISNA = { + description: _t("Whether a value is the error #N/A."), + args: [arg("value (any, lazy)", _t("The value to be verified as an error type."))], + returns: ["BOOLEAN"], + compute: function (value) { + try { + value(); + return false; + } + catch (e) { + return e?.errorType === CellErrorType.NotAvailable; + } + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ISNONTEXT + // ----------------------------------------------------------------------------- + const ISNONTEXT = { + description: _t("Whether a value is non-textual."), + args: [arg("value (any, lazy)", _t("The value to be checked."))], + returns: ["BOOLEAN"], + compute: function (value) { + try { + return typeof value() !== "string"; + } + catch (e) { + return true; + } + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ISNUMBER + // ----------------------------------------------------------------------------- + const ISNUMBER = { + description: _t("Whether a value is a number."), + args: [arg("value (any, lazy)", _t("The value to be verified as a number."))], + returns: ["BOOLEAN"], + compute: function (value) { + try { + return typeof value() === "number"; + } + catch (e) { + return false; + } + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ISTEXT + // ----------------------------------------------------------------------------- + const ISTEXT = { + description: _t("Whether a value is text."), + args: [arg("value (any, lazy)", _t("The value to be verified as text."))], + returns: ["BOOLEAN"], + compute: function (value) { + try { + return typeof value() === "string"; + } + catch (e) { + return false; + } + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ISBLANK + // ----------------------------------------------------------------------------- + const ISBLANK = { + description: _t("Whether the referenced cell is empty"), + args: [arg("value (any, lazy)", _t("Reference to the cell that will be checked for emptiness."))], + returns: ["BOOLEAN"], + compute: function (value) { + try { + const val = value(); + return val === null; + } + catch (e) { + return false; + } + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // NA + // ----------------------------------------------------------------------------- + const NA = { + description: _t("Returns the error value #N/A."), + args: [], + returns: ["BOOLEAN"], + compute: function (value) { + throw new NotAvailableError(); + }, + isExported: true, + }; + + var info = /*#__PURE__*/Object.freeze({ + __proto__: null, + ISBLANK: ISBLANK, + ISERR: ISERR, + ISERROR: ISERROR, + ISLOGICAL: ISLOGICAL, + ISNA: ISNA, + ISNONTEXT: ISNONTEXT, + ISNUMBER: ISNUMBER, + ISTEXT: ISTEXT, + NA: NA + }); + + // ----------------------------------------------------------------------------- + // AND + // ----------------------------------------------------------------------------- + const AND = { + description: _t("Logical `and` operator."), + args: [ + arg("logical_expression1 (boolean, range)", _t("An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE, or an expression that can be coerced to a logical value.")), + arg("logical_expression2 (boolean, range, repeating)", _t("More expressions that represent logical values.")), + ], + returns: ["BOOLEAN"], + compute: function (...logicalExpressions) { + let foundBoolean = false; + let acc = true; + conditionalVisitBoolean(logicalExpressions, (arg) => { + foundBoolean = true; + acc = acc && arg; + return acc; + }); + assert(() => foundBoolean, _t("[[FUNCTION_NAME]] has no valid input data.")); + return acc; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // FALSE + // ----------------------------------------------------------------------------- + const FALSE = { + description: _t("Logical value `false`."), + args: [], + returns: ["BOOLEAN"], + compute: function () { + return false; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // IF + // ----------------------------------------------------------------------------- + const IF = { + description: _t("Returns value depending on logical expression."), + args: [ + arg("logical_expression (boolean)", _t("An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE.")), + arg("value_if_true (any, lazy)", _t("The value the function returns if logical_expression is TRUE.")), + arg("value_if_false (any, lazy, default=FALSE)", _t("The value the function returns if logical_expression is FALSE.")), + ], + returns: ["ANY"], + computeValueAndFormat: function (logicalExpression, valueIfTrue, valueIfFalse = () => ({ value: false })) { + const result = toBoolean(logicalExpression?.value) ? valueIfTrue() : valueIfFalse(); + if (result === undefined) { + return { value: "" }; + } + if (result.value === null) { + result.value = ""; + } + return result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // IFERROR + // ----------------------------------------------------------------------------- + const IFERROR = { + description: _t("Value if it is not an error, otherwise 2nd argument."), + args: [ + arg("value (any, lazy)", _t("The value to return if value itself is not an error.")), + arg(`value_if_error (any, lazy, default="empty")`, _t("The value the function returns if value is an error.")), + ], + returns: ["ANY"], + computeValueAndFormat: function (value, valueIfError = () => ({ value: "" })) { + let result; + try { + result = value(); + } + catch (e) { + result = valueIfError(); + } + if (result === undefined) { + return { value: "" }; + } + if (result.value === null) { + result.value = ""; + } + return result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // IFNA + // ----------------------------------------------------------------------------- + const IFNA = { + description: _t("Value if it is not an #N/A error, otherwise 2nd argument."), + args: [ + arg("value (any, lazy)", _t("The value to return if value itself is not #N/A an error.")), + arg(`value_if_error (any, lazy, default="empty")`, _t("The value the function returns if value is an #N/A error.")), + ], + returns: ["ANY"], + computeValueAndFormat: function (value, valueIfError = () => ({ value: "" })) { + let result; + try { + result = value(); + } + catch (e) { + if (e.errorType === CellErrorType.NotAvailable) { + result = valueIfError(); + } + else { + result = value(); + } + } + if (result === undefined) { + return { value: "" }; + } + if (result.value === null) { + result.value = ""; + } + return result; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // IFS + // ----------------------------------------------------------------------------- + const IFS = { + description: _t("Returns a value depending on multiple logical expressions."), + args: [ + arg("condition1 (boolean, lazy)", _t("The first condition to be evaluated. This can be a boolean, a number, an array, or a reference to any of those.")), + arg("value1 (any, lazy)", _t("The returned value if condition1 is TRUE.")), + arg("condition2 (boolean, lazy, repeating)", _t("Additional conditions to be evaluated if the previous ones are FALSE.")), + arg("value2 (any, lazy, repeating)", _t("Additional values to be returned if their corresponding conditions are TRUE.")), + ], + returns: ["ANY"], + computeValueAndFormat: function (...values) { + assert(() => values.length % 2 === 0, _t("Wrong number of arguments. Expected an even number of arguments.")); + for (let n = 0; n < values.length - 1; n += 2) { + if (toBoolean(values[n]()?.value)) { + const result = values[n + 1](); + if (result === undefined) { + return { value: "" }; + } + if (result.value === null) { + result.value = ""; + } + return result; + } + } + throw new Error(_t("No match.")); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // NOT + // ----------------------------------------------------------------------------- + const NOT = { + description: _t("Returns opposite of provided logical value."), + args: [ + arg("logical_expression (boolean)", _t("An expression or reference to a cell holding an expression that represents some logical value.")), + ], + returns: ["BOOLEAN"], + compute: function (logicalExpression) { + return !toBoolean(logicalExpression); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // OR + // ----------------------------------------------------------------------------- + const OR = { + description: _t("Logical `or` operator."), + args: [ + arg("logical_expression1 (boolean, range)", _t("An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE, or an expression that can be coerced to a logical value.")), + arg("logical_expression2 (boolean, range, repeating)", _t("More expressions that evaluate to logical values.")), + ], + returns: ["BOOLEAN"], + compute: function (...logicalExpressions) { + let foundBoolean = false; + let acc = false; + conditionalVisitBoolean(logicalExpressions, (arg) => { + foundBoolean = true; + acc = acc || arg; + return !acc; + }); + assert(() => foundBoolean, _t("[[FUNCTION_NAME]] has no valid input data.")); + return acc; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TRUE + // ----------------------------------------------------------------------------- + const TRUE = { + description: _t("Logical value `true`."), + args: [], + returns: ["BOOLEAN"], + compute: function () { + return true; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // XOR + // ----------------------------------------------------------------------------- + const XOR = { + description: _t("Logical `xor` operator."), + args: [ + arg("logical_expression1 (boolean, range)", _t("An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE, or an expression that can be coerced to a logical value.")), + arg("logical_expression2 (boolean, range, repeating)", _t("More expressions that evaluate to logical values.")), + ], + returns: ["BOOLEAN"], + compute: function (...logicalExpressions) { + let foundBoolean = false; + let acc = false; + conditionalVisitBoolean(logicalExpressions, (arg) => { + foundBoolean = true; + acc = acc ? !arg : arg; + return true; // no stop condition + }); + assert(() => foundBoolean, _t("[[FUNCTION_NAME]] has no valid input data.")); + return acc; + }, + isExported: true, + }; + + var logical = /*#__PURE__*/Object.freeze({ + __proto__: null, + AND: AND, + FALSE: FALSE, + IF: IF, + IFERROR: IFERROR, + IFNA: IFNA, + IFS: IFS, + NOT: NOT, + OR: OR, + TRUE: TRUE, + XOR: XOR + }); + + const DEFAULT_IS_SORTED = true; + const DEFAULT_MATCH_MODE = 0; + const DEFAULT_SEARCH_MODE = 1; + const DEFAULT_ABSOLUTE_RELATIVE_MODE = 1; + function assertAvailable(variable, searchKey) { + if (variable === undefined) { + throw new NotAvailableError(_t("Did not find value '%s' in [[FUNCTION_NAME]] evaluation.", toString(searchKey))); + } + } + // ----------------------------------------------------------------------------- + // ADDRESS + // ----------------------------------------------------------------------------- + const ADDRESS = { + description: _t("Returns a cell reference as a string. "), + args: [ + arg("row (number)", _t("The row number of the cell reference. ")), + arg("column (number)", _t("The column number (not name) of the cell reference. A is column number 1. ")), + arg(`absolute_relative_mode (number, default=${DEFAULT_ABSOLUTE_RELATIVE_MODE})`, _t("An indicator of whether the reference is row/column absolute. 1 is row and column absolute (e.g. $A$1), 2 is row absolute and column relative (e.g. A$1), 3 is row relative and column absolute (e.g. $A1), and 4 is row and column relative (e.g. A1).")), + arg("use_a1_notation (boolean, default=TRUE)", _t("A boolean indicating whether to use A1 style notation (TRUE) or R1C1 style notation (FALSE).")), + arg("sheet (string, optional)", _t("A string indicating the name of the sheet into which the address points.")), + ], + returns: ["STRING"], + compute: function (row, column, absoluteRelativeMode = DEFAULT_ABSOLUTE_RELATIVE_MODE, useA1Notation = true, sheet) { + const rowNumber = strictToInteger(row, this.locale); + const colNumber = strictToInteger(column, this.locale); + assertNumberGreaterThanOrEqualToOne(rowNumber); + assertNumberGreaterThanOrEqualToOne(colNumber); + const _absoluteRelativeMode = strictToInteger(absoluteRelativeMode, this.locale); + assert(() => [1, 2, 3, 4].includes(_absoluteRelativeMode), expectNumberRangeError(1, 4, _absoluteRelativeMode)); + const _useA1Notation = toBoolean(useA1Notation); + let cellReference; + if (_useA1Notation) { + const rangePart = { + rowFixed: [1, 2].includes(_absoluteRelativeMode) ? true : false, + colFixed: [1, 3].includes(_absoluteRelativeMode) ? true : false, + }; + cellReference = toXC(colNumber - 1, rowNumber - 1, rangePart); + } + else { + const rowPart = [1, 2].includes(_absoluteRelativeMode) ? `R${rowNumber}` : `R[${rowNumber}]`; + const colPart = [1, 3].includes(_absoluteRelativeMode) ? `C${colNumber}` : `C[${colNumber}]`; + cellReference = rowPart + colPart; + } + if (sheet !== undefined) { + return `${getCanonicalSheetName(toString(sheet))}!${cellReference}`; + } + return cellReference; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COLUMN + // ----------------------------------------------------------------------------- + const COLUMN = { + description: _t("Column number of a specified cell."), + args: [ + arg("cell_reference (meta, default='this cell')", _t("The cell whose column number will be returned. Column A corresponds to 1. By default, the function use the cell in which the formula is entered.")), + ], + returns: ["NUMBER"], + compute: function (cellReference) { + const _cellReference = cellReference || this.__originCellXC?.(); + assert(() => !!_cellReference, "In this context, the function [[FUNCTION_NAME]] needs to have a cell or range in parameter."); + const zone = toZone(_cellReference); + return zone.left + 1; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // COLUMNS + // ----------------------------------------------------------------------------- + const COLUMNS = { + description: _t("Number of columns in a specified array or range."), + args: [arg("range (meta)", _t("The range whose column count will be returned."))], + returns: ["NUMBER"], + compute: function (range) { + const zone = toZone(range); + return zone.right - zone.left + 1; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // HLOOKUP + // ----------------------------------------------------------------------------- + const HLOOKUP = { + description: _t("Horizontal lookup"), + args: [ + arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")), + arg("range (range)", _t("The range to consider for the search. The first row in the range is searched for the key specified in search_key.")), + arg("index (number)", _t("The row index of the value to be returned, where the first row in range is numbered 1.")), + arg(`is_sorted (boolean, default=${DEFAULT_IS_SORTED})`, _t("Indicates whether the row to be searched (the first row of the specified range) is sorted, in which case the closest match for search_key will be returned.")), + ], + returns: ["ANY"], + computeValueAndFormat: function (searchKey, range, index, isSorted = { value: DEFAULT_IS_SORTED }) { + const _index = Math.trunc(toNumber(index?.value, this.locale)); + assert(() => 1 <= _index && _index <= range[0].length, _t("[[FUNCTION_NAME]] evaluates to an out of bounds range.")); + const getValueFromRange = (range, index) => range[index][0].value; + const _isSorted = toBoolean(isSorted.value); + const colIndex = _isSorted + ? dichotomicSearch(range, searchKey?.value, "nextSmaller", "asc", range.length, getValueFromRange) + : linearSearch(range, searchKey?.value, "strict", range.length, getValueFromRange); + const col = range[colIndex]; + assertAvailable(col, searchKey?.value); + return col[_index - 1]; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // INDEX + // ----------------------------------------------------------------------------- + const INDEX = { + description: _t("Returns the content of a cell, specified by row and column offset."), + args: [ + arg("reference (any, range)", _t("The range of cells from which the values are returned.")), + arg("row (number, default=0)", _t("The index of the row to be returned from within the reference range of cells.")), + arg("column (number, default=0)", _t("The index of the column to be returned from within the reference range of cells.")), + ], + returns: ["ANY"], + computeValueAndFormat: function (reference, row = { value: 0 }, column = { value: 0 }) { + const _reference = isMatrix(reference) ? reference : [[reference]]; + const _row = toNumber(row.value, this.locale); + const _column = toNumber(column.value, this.locale); + assert(() => _column >= 0 && + _column - 1 < _reference.length && + _row >= 0 && + _row - 1 < _reference[0].length, _t("Index out of range.")); + if (_row === 0 && _column === 0) { + return _reference; + } + if (_row === 0) { + return [_reference[_column - 1]]; + } + if (_column === 0) { + return _reference.map((col) => [col[_row - 1]]); + } + return _reference[_column - 1][_row - 1]; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // LOOKUP + // ----------------------------------------------------------------------------- + const LOOKUP = { + description: _t("Look up a value."), + args: [ + arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")), + arg("search_array (range)", _t("One method of using this function is to provide a single sorted row or column search_array to look through for the search_key with a second argument result_range. The other way is to combine these two arguments into one search_array where the first row or column is searched and a value is returned from the last row or column in the array. If search_key is not found, a non-exact match may be returned.")), + arg("result_range (range, optional)", _t("The range from which to return a result. The value returned corresponds to the location where search_key is found in search_range. This range must be only a single row or column and should not be used if using the search_result_array method.")), + ], + returns: ["ANY"], + computeValueAndFormat: function (searchKey, searchArray, resultRange) { + let nbCol = searchArray.length; + let nbRow = searchArray[0].length; + const verticalSearch = nbRow >= nbCol; + const getElement = verticalSearch + ? (range, index) => range[0][index].value + : (range, index) => range[index][0].value; + const rangeLength = verticalSearch ? nbRow : nbCol; + const index = dichotomicSearch(searchArray, searchKey?.value, "nextSmaller", "asc", rangeLength, getElement); + if (index === -1) + assertAvailable(undefined, searchKey?.value); + verticalSearch + ? assertAvailable(searchArray[0][index], searchKey?.value) + : assertAvailable(searchArray[index][nbRow - 1], searchKey?.value); + if (resultRange === undefined) { + return verticalSearch ? searchArray[nbCol - 1][index] : searchArray[index][nbRow - 1]; + } + nbCol = resultRange.length; + nbRow = resultRange[0].length; + assert(() => nbCol === 1 || nbRow === 1, _t("The result_range must be a single row or a single column.")); + if (nbCol > 1) { + assert(() => index <= nbCol - 1, _t("[[FUNCTION_NAME]] evaluates to an out of range row value %s.", (index + 1).toString())); + return resultRange[index][0]; + } + assert(() => index <= nbRow - 1, _t("[[FUNCTION_NAME]] evaluates to an out of range column value %s.", (index + 1).toString())); + return resultRange[0][index]; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MATCH + // ----------------------------------------------------------------------------- + const DEFAULT_SEARCH_TYPE = 1; + const MATCH = { + description: _t("Position of item in range that matches value."), + args: [ + arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")), + arg("range (any, range)", _t("The one-dimensional array to be searched.")), + arg(`search_type (number, default=${DEFAULT_SEARCH_TYPE})`, _t("The search method. 1 (default) finds the largest value less than or equal to search_key when range is sorted in ascending order. 0 finds the exact value when range is unsorted. -1 finds the smallest value greater than or equal to search_key when range is sorted in descending order.")), + ], + returns: ["NUMBER"], + compute: function (searchKey, range, searchType = DEFAULT_SEARCH_TYPE) { + let _searchType = toNumber(searchType, this.locale); + const nbCol = range.length; + const nbRow = range[0].length; + assert(() => nbCol === 1 || nbRow === 1, _t("The range must be a single row or a single column.")); + let index = -1; + const getElement = nbCol === 1 + ? (range, index) => range[0][index] + : (range, index) => range[index][0]; + const rangeLen = nbCol === 1 ? range[0].length : range.length; + _searchType = Math.sign(_searchType); + switch (_searchType) { + case 1: + index = dichotomicSearch(range, searchKey, "nextSmaller", "asc", rangeLen, getElement); + break; + case 0: + index = linearSearch(range, searchKey, "strict", rangeLen, getElement); + break; + case -1: + index = dichotomicSearch(range, searchKey, "nextGreater", "desc", rangeLen, getElement); + break; + } + assertAvailable(nbCol === 1 ? range[0][index] : range[index], searchKey); + return index + 1; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ROW + // ----------------------------------------------------------------------------- + const ROW = { + description: _t("Row number of a specified cell."), + args: [ + arg("cell_reference (meta, default='this cell')", _t("The cell whose row number will be returned. By default, this function uses the cell in which the formula is entered.")), + ], + returns: ["NUMBER"], + compute: function (cellReference) { + cellReference = cellReference || this.__originCellXC?.(); + assert(() => !!cellReference, "In this context, the function [[FUNCTION_NAME]] needs to have a cell or range in parameter."); + const zone = toZone(cellReference); + return zone.top + 1; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // ROWS + // ----------------------------------------------------------------------------- + const ROWS = { + description: _t("Number of rows in a specified array or range."), + args: [arg("range (meta)", _t("The range whose row count will be returned."))], + returns: ["NUMBER"], + compute: function (range) { + const zone = toZone(range); + return zone.bottom - zone.top + 1; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // VLOOKUP + // ----------------------------------------------------------------------------- + const VLOOKUP = { + description: _t("Vertical lookup."), + args: [ + arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")), + arg("range (any, range)", _t("The range to consider for the search. The first column in the range is searched for the key specified in search_key.")), + arg("index (number)", _t("The column index of the value to be returned, where the first column in range is numbered 1.")), + arg(`is_sorted (boolean, default=${DEFAULT_IS_SORTED})`, _t("Indicates whether the column to be searched (the first column of the specified range) is sorted, in which case the closest match for search_key will be returned.")), + ], + returns: ["ANY"], + computeValueAndFormat: function (searchKey, range, index, isSorted = { value: DEFAULT_IS_SORTED }) { + const _index = Math.trunc(toNumber(index?.value, this.locale)); + assert(() => 1 <= _index && _index <= range.length, _t("[[FUNCTION_NAME]] evaluates to an out of bounds range.")); + const getValueFromRange = (range, index) => range[0][index].value; + const _isSorted = toBoolean(isSorted.value); + const rowIndex = _isSorted + ? dichotomicSearch(range, searchKey?.value, "nextSmaller", "asc", range[0].length, getValueFromRange) + : linearSearch(range, searchKey?.value, "strict", range[0].length, getValueFromRange); + const value = range[_index - 1][rowIndex]; + assertAvailable(value, searchKey); + return value; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // XLOOKUP + // ----------------------------------------------------------------------------- + const XLOOKUP = { + description: _t("Search a range for a match and return the corresponding item from a second range."), + args: [ + arg("search_key (any)", _t("The value to search for.")), + arg("lookup_range (any, range)", _t("The range to consider for the search. Should be a single column or a single row.")), + arg("return_range (any, range)", _t("The range containing the return value. Should have the same dimensions as lookup_range.")), + arg("if_not_found (any, lazy, optional)", _t("If a valid match is not found, return this value.")), + arg(`match_mode (any, default=${DEFAULT_MATCH_MODE})`, _t("(0) Exact match. (-1) Return next smaller item if no match. (1) Return next greater item if no match.")), + arg(`search_mode (any, default=${DEFAULT_SEARCH_MODE})`, _t("(1) Search starting at first item. \ + (-1) Search starting at last item. \ + (2) Perform a binary search that relies on lookup_array being sorted in ascending order. If not sorted, invalid results will be returned. \ + (-2) Perform a binary search that relies on lookup_array being sorted in descending order. If not sorted, invalid results will be returned.\ + ")), + ], + returns: ["ANY"], + computeValueAndFormat: function (searchKey, lookupRange, returnRange, defaultValue, matchMode = { value: DEFAULT_MATCH_MODE }, searchMode = { value: DEFAULT_SEARCH_MODE }) { + const _matchMode = Math.trunc(toNumber(matchMode.value, this.locale)); + const _searchMode = Math.trunc(toNumber(searchMode.value, this.locale)); + assert(() => lookupRange.length === 1 || lookupRange[0].length === 1, _t("lookup_range should be either a single row or single column.")); + assert(() => [-1, 1, -2, 2].includes(_searchMode), _t("searchMode should be a value in [-1, 1, -2, 2].")); + assert(() => [-1, 0, 1].includes(_matchMode), _t("matchMode should be a value in [-1, 0, 1].")); + const lookupDirection = lookupRange.length === 1 ? "col" : "row"; + assert(() => lookupDirection === "col" + ? returnRange[0].length === lookupRange[0].length + : returnRange.length === lookupRange.length, _t("return_range should have the same dimensions as lookup_range.")); + const getElement = lookupDirection === "col" + ? (range, index) => range[0][index].value + : (range, index) => range[index][0].value; + const rangeLen = lookupDirection === "col" ? lookupRange[0].length : lookupRange.length; + const mode = _matchMode === 0 ? "strict" : _matchMode === 1 ? "nextGreater" : "nextSmaller"; + const reverseSearch = _searchMode === -1; + const index = _searchMode === 2 || _searchMode === -2 + ? dichotomicSearch(lookupRange, searchKey?.value, mode, _searchMode === 2 ? "asc" : "desc", rangeLen, getElement) + : linearSearch(lookupRange, searchKey?.value, mode, rangeLen, getElement, reverseSearch); + if (index !== -1) { + return lookupDirection === "col" + ? returnRange.map((col) => [col[index]]) + : [returnRange[index]]; + } + const _defaultValue = defaultValue?.(); + assertAvailable(_defaultValue, searchKey); + return [[_defaultValue]]; + }, + isExported: true, + }; + + var lookup = /*#__PURE__*/Object.freeze({ + __proto__: null, + ADDRESS: ADDRESS, + COLUMN: COLUMN, + COLUMNS: COLUMNS, + HLOOKUP: HLOOKUP, + INDEX: INDEX, + LOOKUP: LOOKUP, + MATCH: MATCH, + ROW: ROW, + ROWS: ROWS, + VLOOKUP: VLOOKUP, + XLOOKUP: XLOOKUP + }); + + // ----------------------------------------------------------------------------- + // ADD + // ----------------------------------------------------------------------------- + const ADD = { + description: _t("Sum of two numbers."), + args: [ + arg("value1 (number)", _t("The first addend.")), + arg("value2 (number)", _t("The second addend.")), + ], + returns: ["NUMBER"], + computeFormat: (value1, value2) => value1?.format || value2?.format, + compute: function (value1, value2) { + return toNumber(value1, this.locale) + toNumber(value2, this.locale); + }, + }; + // ----------------------------------------------------------------------------- + // CONCAT + // ----------------------------------------------------------------------------- + const CONCAT = { + description: _t("Concatenation of two values."), + args: [ + arg("value1 (string)", _t("The value to which value2 will be appended.")), + arg("value2 (string)", _t("The value to append to value1.")), + ], + returns: ["STRING"], + compute: function (value1, value2) { + return toString(value1) + toString(value2); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // DIVIDE + // ----------------------------------------------------------------------------- + const DIVIDE = { + description: _t("One number divided by another."), + args: [ + arg("dividend (number)", _t("The number to be divided.")), + arg("divisor (number)", _t("The number to divide by.")), + ], + returns: ["NUMBER"], + computeFormat: (dividend, divisor) => dividend?.format || divisor?.format, + compute: function (dividend, divisor) { + const _divisor = toNumber(divisor, this.locale); + assert(() => _divisor !== 0, _t("The divisor must be different from zero.")); + return toNumber(dividend, this.locale) / _divisor; + }, + }; + // ----------------------------------------------------------------------------- + // EQ + // ----------------------------------------------------------------------------- + function isEmpty(value) { + return value === null || value === undefined; + } + const getNeutral = { number: 0, string: "", boolean: false }; + const EQ = { + description: _t("Equal."), + args: [ + arg("value1 (any)", _t("The first value.")), + arg("value2 (any)", _t("The value to test against value1 for equality.")), + ], + returns: ["BOOLEAN"], + compute: function (value1, value2) { + value1 = isEmpty(value1) ? getNeutral[typeof value2] : value1; + value2 = isEmpty(value2) ? getNeutral[typeof value1] : value2; + if (typeof value1 === "string") { + value1 = value1.toUpperCase(); + } + if (typeof value2 === "string") { + value2 = value2.toUpperCase(); + } + return value1 === value2; + }, + }; + // ----------------------------------------------------------------------------- + // GT + // ----------------------------------------------------------------------------- + function applyRelationalOperator(value1, value2, cb) { + value1 = isEmpty(value1) ? getNeutral[typeof value2] : value1; + value2 = isEmpty(value2) ? getNeutral[typeof value1] : value2; + if (typeof value1 !== "number") { + value1 = toString(value1).toUpperCase(); + } + if (typeof value2 !== "number") { + value2 = toString(value2).toUpperCase(); + } + const tV1 = typeof value1; + const tV2 = typeof value2; + if (tV1 === "string" && tV2 === "number") { + return true; + } + if (tV2 === "string" && tV1 === "number") { + return false; + } + return cb(value1, value2); + } + const GT = { + description: _t("Strictly greater than."), + args: [ + arg("value1 (any)", _t("The value to test as being greater than value2.")), + arg("value2 (any)", _t("The second value.")), + ], + returns: ["BOOLEAN"], + compute: function (value1, value2) { + return applyRelationalOperator(value1, value2, (v1, v2) => { + return v1 > v2; + }); + }, + }; + // ----------------------------------------------------------------------------- + // GTE + // ----------------------------------------------------------------------------- + const GTE = { + description: _t("Greater than or equal to."), + args: [ + arg("value1 (any)", _t("The value to test as being greater than or equal to value2.")), + arg("value2 (any)", _t("The second value.")), + ], + returns: ["BOOLEAN"], + compute: function (value1, value2) { + return applyRelationalOperator(value1, value2, (v1, v2) => { + return v1 >= v2; + }); + }, + }; + // ----------------------------------------------------------------------------- + // LT + // ----------------------------------------------------------------------------- + const LT = { + description: _t("Less than."), + args: [ + arg("value1 (any)", _t("The value to test as being less than value2.")), + arg("value2 (any)", _t("The second value.")), + ], + returns: ["BOOLEAN"], + compute: function (value1, value2) { + return !GTE.compute.bind(this)(value1, value2); + }, + }; + // ----------------------------------------------------------------------------- + // LTE + // ----------------------------------------------------------------------------- + const LTE = { + description: _t("Less than or equal to."), + args: [ + arg("value1 (any)", _t("The value to test as being less than or equal to value2.")), + arg("value2 (any)", _t("The second value.")), + ], + returns: ["BOOLEAN"], + compute: function (value1, value2) { + return !GT.compute.bind(this)(value1, value2); + }, + }; + // ----------------------------------------------------------------------------- + // MINUS + // ----------------------------------------------------------------------------- + const MINUS = { + description: _t("Difference of two numbers."), + args: [ + arg("value1 (number)", _t("The minuend, or number to be subtracted from.")), + arg("value2 (number)", _t("The subtrahend, or number to subtract from value1.")), + ], + returns: ["NUMBER"], + computeFormat: (value1, value2) => value1?.format || value2?.format, + compute: function (value1, value2) { + return toNumber(value1, this.locale) - toNumber(value2, this.locale); + }, + }; + // ----------------------------------------------------------------------------- + // MULTIPLY + // ----------------------------------------------------------------------------- + const MULTIPLY = { + description: _t("Product of two numbers"), + args: [ + arg("factor1 (number)", _t("The first multiplicand.")), + arg("factor2 (number)", _t("The second multiplicand.")), + ], + returns: ["NUMBER"], + computeFormat: (factor1, factor2) => factor1?.format || factor2?.format, + compute: function (factor1, factor2) { + return toNumber(factor1, this.locale) * toNumber(factor2, this.locale); + }, + }; + // ----------------------------------------------------------------------------- + // NE + // ----------------------------------------------------------------------------- + const NE = { + description: _t("Not equal."), + args: [ + arg("value1 (any)", _t("The first value.")), + arg("value2 (any)", _t("The value to test against value1 for inequality.")), + ], + returns: ["BOOLEAN"], + compute: function (value1, value2) { + return !EQ.compute.bind(this)(value1, value2); + }, + }; + // ----------------------------------------------------------------------------- + // POW + // ----------------------------------------------------------------------------- + const POW = { + description: _t("A number raised to a power."), + args: [ + arg("base (number)", _t("The number to raise to the exponent power.")), + arg("exponent (number)", _t("The exponent to raise base to.")), + ], + returns: ["NUMBER"], + compute: function (base, exponent) { + return POWER.compute.bind(this)(base, exponent); + }, + }; + // ----------------------------------------------------------------------------- + // UMINUS + // ----------------------------------------------------------------------------- + const UMINUS = { + description: _t("A number with the sign reversed."), + args: [ + arg("value (number)", _t("The number to have its sign reversed. Equivalently, the number to multiply by -1.")), + ], + computeFormat: (value) => value?.format, + returns: ["NUMBER"], + compute: function (value) { + return -toNumber(value, this.locale); + }, + }; + // ----------------------------------------------------------------------------- + // UNARY_PERCENT + // ----------------------------------------------------------------------------- + const UNARY_PERCENT = { + description: _t("Value interpreted as a percentage."), + args: [arg("percentage (number)", _t("The value to interpret as a percentage."))], + returns: ["NUMBER"], + compute: function (percentage) { + return toNumber(percentage, this.locale) / 100; + }, + }; + // ----------------------------------------------------------------------------- + // UPLUS + // ----------------------------------------------------------------------------- + const UPLUS = { + description: _t("A specified number, unchanged."), + args: [arg("value (any)", _t("The number to return."))], + returns: ["ANY"], + computeFormat: (value) => value?.format, + compute: function (value = "") { + return value === null ? "" : value; + }, + }; + + var operators = /*#__PURE__*/Object.freeze({ + __proto__: null, + ADD: ADD, + CONCAT: CONCAT, + DIVIDE: DIVIDE, + EQ: EQ, + GT: GT, + GTE: GTE, + LT: LT, + LTE: LTE, + MINUS: MINUS, + MULTIPLY: MULTIPLY, + NE: NE, + POW: POW, + UMINUS: UMINUS, + UNARY_PERCENT: UNARY_PERCENT, + UPLUS: UPLUS + }); + + const DEFAULT_STARTING_AT = 1; + /** Regex matching all the words in a string */ + const wordRegex = /[A-Za-zÀ-ÖØ-öø-ÿ]+/g; + // ----------------------------------------------------------------------------- + // CHAR + // ----------------------------------------------------------------------------- + const CHAR = { + description: _t("Gets character associated with number."), + args: [ + arg("table_number (number)", _t("The number of the character to look up from the current Unicode table in decimal format.")), + ], + returns: ["STRING"], + compute: function (tableNumber) { + const _tableNumber = Math.trunc(toNumber(tableNumber, this.locale)); + assert(() => _tableNumber >= 1, _t("The table_number (%s) is out of range.", _tableNumber.toString())); + return String.fromCharCode(_tableNumber); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // CLEAN + // ----------------------------------------------------------------------------- + const CLEAN = { + description: _t("Remove non-printable characters from a piece of text."), + args: [arg("text (string)", _t("The text whose non-printable characters are to be removed."))], + returns: ["STRING"], + compute: function (text) { + const _text = toString(text); + let cleanedStr = ""; + for (const char of _text) { + if (char && char.charCodeAt(0) > 31) { + cleanedStr += char; + } + } + return cleanedStr; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // CONCATENATE + // ----------------------------------------------------------------------------- + const CONCATENATE = { + description: _t("Appends strings to one another."), + args: [ + arg("string1 (string, range)", _t("The initial string.")), + arg("string2 (string, range, repeating)", _t("More strings to append in sequence.")), + ], + returns: ["STRING"], + compute: function (...values) { + return reduceAny(values, (acc, a) => acc + toString(a), ""); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // EXACT + // ----------------------------------------------------------------------------- + const EXACT = { + description: _t("Tests whether two strings are identical."), + args: [ + arg("string1 (string)", _t("The first string to compare.")), + arg("string2 (string)", _t("The second string to compare.")), + ], + returns: ["BOOLEAN"], + compute: function (string1, string2) { + return toString(string1) === toString(string2); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // FIND + // ----------------------------------------------------------------------------- + const FIND = { + description: _t("First position of string found in text, case-sensitive."), + args: [ + arg("search_for (string)", _t("The string to look for within text_to_search.")), + arg("text_to_search (string)", _t("The text to search for the first occurrence of search_for.")), + arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search.")), + ], + returns: ["NUMBER"], + compute: function (searchFor, textToSearch, startingAt = DEFAULT_STARTING_AT) { + const _searchFor = toString(searchFor); + const _textToSearch = toString(textToSearch); + const _startingAt = toNumber(startingAt, this.locale); + assert(() => _textToSearch !== "", _t("The text_to_search must be non-empty.")); + assert(() => _startingAt >= 1, _t("The starting_at (%s) must be greater than or equal to 1.", _startingAt.toString())); + const result = _textToSearch.indexOf(_searchFor, _startingAt - 1); + assert(() => result >= 0, _t("In [[FUNCTION_NAME]] evaluation, cannot find '%s' within '%s'.", _searchFor.toString(), _textToSearch)); + return result + 1; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // JOIN + // ----------------------------------------------------------------------------- + const JOIN = { + description: _t("Concatenates elements of arrays with delimiter."), + args: [ + arg("delimiter (string)", _t("The character or string to place between each concatenated value.")), + arg("value_or_array1 (string, range)", _t("The value or values to be appended using delimiter.")), + arg("value_or_array2 (string, range, repeating)", _t("More values to be appended using delimiter.")), + ], + returns: ["STRING"], + compute: function (delimiter, ...valuesOrArrays) { + const _delimiter = toString(delimiter); + return reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toString(a), ""); + }, + }; + // ----------------------------------------------------------------------------- + // LEFT + // ----------------------------------------------------------------------------- + const LEFT = { + description: _t("Substring from beginning of specified string."), + args: [ + arg("text (string)", _t("The string from which the left portion will be returned.")), + arg("number_of_characters (number, optional)", _t("The number of characters to return from the left side of string.")), + ], + returns: ["STRING"], + compute: function (text, ...args) { + const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1; + assert(() => _numberOfCharacters >= 0, _t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters.toString())); + return toString(text).substring(0, _numberOfCharacters); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // LEN + // ----------------------------------------------------------------------------- + const LEN = { + description: _t("Length of a string."), + args: [arg("text (string)", _t("The string whose length will be returned."))], + returns: ["NUMBER"], + compute: function (text) { + return toString(text).length; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // LOWER + // ----------------------------------------------------------------------------- + const LOWER = { + description: _t("Converts a specified string to lowercase."), + args: [arg("text (string)", _t("The string to convert to lowercase."))], + returns: ["STRING"], + compute: function (text) { + return toString(text).toLowerCase(); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // MID + // ----------------------------------------------------------------------------- + const MID = { + description: _t("A segment of a string."), + args: [ + arg("text (string)", _t("The string to extract a segment from.")), + arg(" (number)", _t("The index from the left of string from which to begin extracting. The first character in string has the index 1.")), + arg(" (number)", _t("The length of the segment to extract.")), + ], + returns: ["STRING"], + compute: function (text, starting_at, extract_length) { + const _text = toString(text); + const _starting_at = toNumber(starting_at, this.locale); + const _extract_length = toNumber(extract_length, this.locale); + assert(() => _starting_at >= 1, _t("The starting_at argument (%s) must be positive greater than one.", _starting_at.toString())); + assert(() => _extract_length >= 0, _t("The extract_length argument (%s) must be positive or null.", _extract_length.toString())); + return _text.slice(_starting_at - 1, _starting_at + _extract_length - 1); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // PROPER + // ----------------------------------------------------------------------------- + const PROPER = { + description: _t("Capitalizes each word in a specified string."), + 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.")), + ], + returns: ["STRING"], + compute: function (text) { + const _text = toString(text); + return _text.replace(wordRegex, (word) => { + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + }); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // REPLACE + // ----------------------------------------------------------------------------- + const REPLACE = { + description: _t("Replaces part of a text string with different text."), + args: [ + arg("text (string)", _t("The text, a part of which will be replaced.")), + arg("position (number)", _t("The position where the replacement will begin (starting from 1).")), + arg("length (number)", _t("The number of characters in the text to be replaced.")), + arg("new_text (string)", _t("The text which will be inserted into the original text.")), + ], + returns: ["STRING"], + compute: function (text, position, length, newText) { + const _position = toNumber(position, this.locale); + assert(() => _position >= 1, _t("The position (%s) must be greater than or equal to 1.", _position.toString())); + const _text = toString(text); + const _length = toNumber(length, this.locale); + const _newText = toString(newText); + return _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // RIGHT + // ----------------------------------------------------------------------------- + const RIGHT = { + description: _t("A substring from the end of a specified string."), + args: [ + arg("text (string)", _t("The string from which the right portion will be returned.")), + arg("number_of_characters (number, optional)", _t("The number of characters to return from the right side of string.")), + ], + returns: ["STRING"], + compute: function (text, ...args) { + const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1; + assert(() => _numberOfCharacters >= 0, _t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters.toString())); + const _text = toString(text); + const stringLength = _text.length; + return _text.substring(stringLength - _numberOfCharacters, stringLength); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SEARCH + // ----------------------------------------------------------------------------- + const SEARCH = { + description: _t("First position of string found in text, ignoring case."), + args: [ + arg("search_for (string)", _t("The string to look for within text_to_search.")), + arg("text_to_search (string)", _t("The text to search for the first occurrence of search_for.")), + arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search.")), + ], + returns: ["NUMBER"], + compute: function (searchFor, textToSearch, startingAt = DEFAULT_STARTING_AT) { + const _searchFor = toString(searchFor).toLowerCase(); + const _textToSearch = toString(textToSearch).toLowerCase(); + const _startingAt = toNumber(startingAt, this.locale); + assert(() => _textToSearch !== "", _t("The text_to_search must be non-empty.")); + assert(() => _startingAt >= 1, _t("The starting_at (%s) must be greater than or equal to 1.", _startingAt.toString())); + const result = _textToSearch.indexOf(_searchFor, _startingAt - 1); + assert(() => result >= 0, _t("In [[FUNCTION_NAME]] evaluation, cannot find '%s' within '%s'.", _searchFor, _textToSearch)); + return result + 1; + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SPLIT + // ----------------------------------------------------------------------------- + const SPLIT_DEFAULT_SPLIT_BY_EACH = true; + const SPLIT_DEFAULT_REMOVE_EMPTY_TEXT = true; + const SPLIT = { + description: _t("Split text by specific character delimiter(s)."), + args: [ + arg("text (string)", _t("The text to divide.")), + arg("delimiter (string)", _t("The character or characters to use to split text.")), + arg(`split_by_each (boolean, default=${SPLIT_DEFAULT_SPLIT_BY_EACH}})`, _t("Whether or not to divide text around each character contained in delimiter.")), + 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.")), + ], + returns: ["RANGE"], + compute: function (text, delimiter, splitByEach = SPLIT_DEFAULT_SPLIT_BY_EACH, removeEmptyText = SPLIT_DEFAULT_REMOVE_EMPTY_TEXT) { + const _text = toString(text); + const _delimiter = escapeRegExp(toString(delimiter)); + const _splitByEach = toBoolean(splitByEach); + const _removeEmptyText = toBoolean(removeEmptyText); + assert(() => _delimiter.length > 0, _t("The _delimiter (%s) must be not be empty.", _delimiter)); + const regex = _splitByEach ? new RegExp(`[${_delimiter}]`, "g") : new RegExp(_delimiter, "g"); + let result = _text.split(regex); + if (_removeEmptyText) { + result = result.filter((text) => text !== ""); + } + return transposeMatrix([result]); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // SUBSTITUTE + // ----------------------------------------------------------------------------- + const SUBSTITUTE = { + description: _t("Replaces existing text with new text in a string."), + args: [ + arg("text_to_search (string)", _t("The text within which to search and replace.")), + arg("search_for (string)", _t("The string to search for within text_to_search.")), + arg("replace_with (string)", _t("The string that will replace search_for.")), + arg("occurrence_number (number, optional)", _t("The instance of search_for within text_to_search to replace with replace_with. By default, all occurrences of search_for are replaced; however, if occurrence_number is specified, only the indicated instance of search_for is replaced.")), + ], + returns: ["NUMBER"], + compute: function (textToSearch, searchFor, replaceWith, occurrenceNumber) { + const _occurrenceNumber = toNumber(occurrenceNumber, this.locale); + assert(() => _occurrenceNumber >= 0, _t("The occurrenceNumber (%s) must be positive or null.", _occurrenceNumber.toString())); + const _textToSearch = toString(textToSearch); + const _searchFor = toString(searchFor); + if (_searchFor === "") { + return _textToSearch; + } + const _replaceWith = toString(replaceWith); + const reg = new RegExp(escapeRegExp(_searchFor), "g"); + if (_occurrenceNumber === 0) { + return _textToSearch.replace(reg, _replaceWith); + } + let n = 0; + return _textToSearch.replace(reg, (text) => (++n === _occurrenceNumber ? _replaceWith : text)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TEXTJOIN + // ----------------------------------------------------------------------------- + const TEXTJOIN = { + description: _t("Combines text from multiple strings and/or arrays."), + args: [ + arg("delimiter (string)", _t(" A string, possible empty, or a reference to a valid string. If empty, the text will be simply concatenated.")), + arg("ignore_empty (boolean)", _t("A boolean; if TRUE, empty cells selected in the text arguments won't be included in the result.")), + arg("text1 (string, range)", _t("Any text item. This could be a string, or an array of strings in a range.")), + arg("text2 (string, range, repeating)", _t("Additional text item(s).")), + ], + returns: ["STRING"], + compute: function (delimiter, ignoreEmpty, ...textsOrArrays) { + const _delimiter = toString(delimiter); + const _ignoreEmpty = toBoolean(ignoreEmpty); + let n = 0; + return reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toString(a) === "") ? (n++ ? acc + _delimiter : "") + toString(a) : acc, ""); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TRIM + // ----------------------------------------------------------------------------- + const TRIM = { + description: _t("Removes space characters."), + args: [ + arg("text (string)", _t("The text or reference to a cell containing text to be trimmed.")), + ], + returns: ["STRING"], + compute: function (text) { + return trimContent(toString(text)); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // UPPER + // ----------------------------------------------------------------------------- + const UPPER = { + description: _t("Converts a specified string to uppercase."), + args: [arg("text (string)", _t("The string to convert to uppercase."))], + returns: ["STRING"], + compute: function (text) { + return toString(text).toUpperCase(); + }, + isExported: true, + }; + // ----------------------------------------------------------------------------- + // TEXT + // ----------------------------------------------------------------------------- + const TEXT = { + description: _t("Converts a number to text according to a specified format."), + args: [ + arg("number (number)", _t("The number, date or time to format.")), + arg("format (string)", _t("The pattern by which to format the number, enclosed in quotation marks.")), + ], + returns: ["STRING"], + compute: function (number, format) { + const _number = toNumber(number, this.locale); + return formatValue(_number, { format: toString(format), locale: this.locale }); + }, + isExported: true, + }; + + var text = /*#__PURE__*/Object.freeze({ + __proto__: null, + CHAR: CHAR, + CLEAN: CLEAN, + CONCATENATE: CONCATENATE, + EXACT: EXACT, + FIND: FIND, + JOIN: JOIN, + LEFT: LEFT, + LEN: LEN, + LOWER: LOWER, + MID: MID, + PROPER: PROPER, + REPLACE: REPLACE, + RIGHT: RIGHT, + SEARCH: SEARCH, + SPLIT: SPLIT, + SUBSTITUTE: SUBSTITUTE, + TEXT: TEXT, + TEXTJOIN: TEXTJOIN, + TRIM: TRIM, + UPPER: UPPER + }); + + // ----------------------------------------------------------------------------- + // HYPERLINK + // ----------------------------------------------------------------------------- + const HYPERLINK = { + description: _t("Creates a hyperlink in a cell."), + 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.")), + ], + returns: ["STRING"], + compute: function (url, linkLabel) { + const processedUrl = toString(url).trim(); + const processedLabel = toString(linkLabel) || processedUrl; + if (processedUrl === "") + return processedLabel; + return markdownLink(processedLabel, processedUrl); + }, + isExported: true, + }; + + var web = /*#__PURE__*/Object.freeze({ + __proto__: null, + HYPERLINK: HYPERLINK + }); + + const categories = [ + { name: _t("Array"), functions: array }, + { name: _t("Database"), functions: database }, + { name: _t("Date"), functions: date }, + { name: _t("Filter"), functions: filter }, + { name: _t("Financial"), functions: financial }, + { name: _t("Info"), functions: info }, + { name: _t("Lookup"), functions: lookup }, + { name: _t("Logical"), functions: logical }, + { name: _t("Math"), functions: math }, + { name: _t("Misc"), functions: misc }, + { name: _t("Operator"), functions: operators }, + { name: _t("Statistical"), functions: statistical }, + { name: _t("Text"), functions: text }, + { name: _t("Engineering"), functions: engineering }, + { name: _t("Web"), functions: web }, + ]; + const functionNameRegex = /^[A-Z0-9\_\.]+$/; + //------------------------------------------------------------------------------ + // Function registry + //------------------------------------------------------------------------------ + class FunctionRegistry extends Registry { + mapping = {}; + add(name, addDescr) { + name = name.toUpperCase(); + if (!functionNameRegex.test(name)) { + throw new Error(_t("Invalid function name %s. Function names can exclusively contain alphanumerical values separated by dots (.) or underscore (_)", name)); + } + const descr = addMetaInfoFromArg(addDescr); + validateArguments(descr.args); + this.mapping[name] = createComputeFunctionFromDescription(descr); + super.add(name, descr); + return this; + } + } + function createComputeFunctionFromDescription(descr) { + const computeValueAndFormat = "computeValueAndFormat" in descr; + const computeValue = "compute" in descr; + const computeFormat = "computeFormat" in descr; + if (!computeValueAndFormat && !computeValue) { + throw new Error("Invalid function description, need at least one 'compute' or 'computeValueAndFormat' function"); + } + if (computeValueAndFormat && (computeValue || computeFormat)) { + throw new Error("Invalid function description, cannot have both 'computeValueAndFormat' and 'compute'/'computeFormat' functions"); + } + if (computeValueAndFormat) { + return descr.computeValueAndFormat; + } + // case computeValue + return buildComputeFunctionFromDescription(descr); + } + function buildComputeFunctionFromDescription(descr) { + return function (...args) { + const computeValue = descr.compute.bind(this); + const computeFormat = descr.computeFormat?.bind(this) || (() => undefined); + const value = computeValue(...extractArgValuesFromArgs(args)); + const format = computeFormat(...args); + if (isMatrix(value)) { + if (format === undefined || isMatrix(format)) { + return value.map((col, i) => col.map((row, j) => ({ value: row, format: format?.[i]?.[j] }))); + } + } + else { + if (format === undefined || !isMatrix(format)) { + return { value, format }; + } + } + throw new Error("A format matrix should never be associated with a scalar value"); + }; + } + function extractArgValuesFromArgs(args) { + return args.map((arg) => { + if (arg === undefined) { + return undefined; + } + if (typeof arg === "function") { + return () => extractArgValueFromArg(arg()); + } + return extractArgValueFromArg(arg); + }); + } + function extractArgValueFromArg(arg) { + if (isMatrix(arg)) { + return matrixMap(arg, (data) => data.value); + } + return arg?.value; + } + const functionRegistry = new FunctionRegistry(); + for (let category of categories) { + const fns = category.functions; + for (let name in fns) { + const addDescr = fns[name]; + addDescr.category = addDescr.category || category.name; + name = name.replace(/_/g, "."); + functionRegistry.add(name, { isExported: false, ...addDescr }); + } + } + const insertRow = { name: (env) => { const number = getRowsNumber(env); @@ -20284,105 +19715,6 @@ separator: true, }); - const SORT_TYPES = [ - CellValueType.number, - CellValueType.error, - CellValueType.text, - CellValueType.boolean, - ]; - function sortCells(cells, sortDirection, emptyCellAsZero) { - const cellsWithIndex = cells.map((cell, index) => ({ - index, - type: cell.type, - value: cell.value, - })); - let emptyCells = cellsWithIndex.filter((x) => x.type === CellValueType.empty); - let nonEmptyCells = cellsWithIndex.filter((x) => x.type !== CellValueType.empty); - if (emptyCellAsZero) { - nonEmptyCells.push(...emptyCells.map((emptyCell) => ({ ...emptyCell, type: CellValueType.number, value: 0 }))); - emptyCells = []; - } - const inverse = sortDirection === "descending" ? -1 : 1; - return nonEmptyCells - .sort((left, right) => { - let typeOrder = SORT_TYPES.indexOf(left.type) - SORT_TYPES.indexOf(right.type); - if (typeOrder === 0) { - if (left.type === CellValueType.text || left.type === CellValueType.error) { - typeOrder = left.value.localeCompare(right.value); - } - else - typeOrder = left.value - right.value; - } - return inverse * typeOrder; - }) - .concat(emptyCells); - } - function interactiveSortSelection(env, sheetId, anchor, zone, sortDirection) { - let result = DispatchResult.Success; - //several columns => bypass the contiguity check - let multiColumns = zone.right > zone.left; - if (env.model.getters.doesIntersectMerge(sheetId, zone)) { - multiColumns = false; - let table; - for (let row = zone.top; row <= zone.bottom; row++) { - table = []; - for (let col = zone.left; col <= zone.right; col++) { - let merge = env.model.getters.getMerge({ sheetId, col, row }); - if (merge && !table.includes(merge.id.toString())) { - table.push(merge.id.toString()); - } - } - if (table.length >= 2) { - multiColumns = true; - break; - } - } - } - const { col, row } = anchor; - if (multiColumns) { - result = env.model.dispatch("SORT_CELLS", { sheetId, col, row, zone, sortDirection }); - } - else { - // check contiguity - const contiguousZone = env.model.getters.getContiguousZone(sheetId, zone); - if (isEqual(contiguousZone, zone)) { - // merge as it is - result = env.model.dispatch("SORT_CELLS", { - sheetId, - col, - row, - zone, - sortDirection, - }); - } - else { - env.askConfirmation(_t("We found data next to your selection. Since this data was not selected, it will not be sorted. Do you want to extend your selection?"), () => { - zone = contiguousZone; - result = env.model.dispatch("SORT_CELLS", { - sheetId, - col, - row, - zone, - sortDirection, - }); - }, () => { - result = env.model.dispatch("SORT_CELLS", { - sheetId, - col, - row, - zone, - sortDirection, - }); - }); - } - } - if (result.isCancelledBecause("InvalidSortZone" /* CommandResult.InvalidSortZone */)) { - const { col, row } = anchor; - env.model.selection.selectZone({ cell: { col, row }, zone }); - env.raiseError(_t("Cannot sort. To sort, select only cells or only merges that have the same size.")); - } - } - const AddFilterInteractiveContent = { filterOverlap: _t("You cannot create overlapping filters."), nonContinuousTargets: _t("A filter can only be created on a continuous selection."), @@ -20550,7 +19882,7 @@ descriptionValue: EXAMPLE_DATE, format: (env) => { const locale = env.model.getters.getLocale(); - return locale.dateFormat + " " + locale.timeFormat; + return getDateTimeFormat(locale); }, }); const formatNumberDuration = createFormatActionSpec({ @@ -22072,16 +21404,11 @@ this.triggerChange(); } reset() { - const existingSelectionRanges = this.env.model.getters.getSelectionInput(this.id); - for (const range of existingSelectionRanges) { - this.env.model.dispatch("REMOVE_RANGE", { - id: this.id, - rangeId: range.id, - }); - } - for (const range of this.previousRanges) { - this.env.model.dispatch("ADD_RANGE", { id: this.id, value: range }); - } + this.env.model.dispatch("ENABLE_NEW_SELECTION_INPUT", { + id: this.id, + initialRanges: this.previousRanges, + hasSingleRange: this.props.hasSingleRange, + }); this.confirm(); } confirm() { @@ -22090,11 +21417,13 @@ const existingSelectionXcs = []; for (const range of existingSelectionRanges) { if (range.xc === "") { - this.env.model.dispatch("REMOVE_RANGE", { + const result = this.env.model.dispatch("REMOVE_RANGE", { id: this.id, rangeId: range.id, }); - continue; + if (result.isSuccessful) { + continue; + } } existingSelectionXcs.push(range.xc); if (this.env.model.getters.isRangeValid(range.xc)) { @@ -24758,7 +24087,7 @@ } get dateTimeFormatPreview() { const locale = this.env.model.getters.getLocale(); - const dateTimeFormat = locale.dateFormat + " " + locale.timeFormat; + const dateTimeFormat = getDateTimeFormat(locale); return formatValue(1.6, { format: dateTimeFormat, locale }); } get currentLocale() { @@ -26565,7 +25894,6 @@ border-top-right-radius: 4px; font-size: ${DEFAULT_FONT_SIZE}; color: white; - opacity: 0; pointer-events: none; } `; @@ -26585,7 +25913,6 @@ left: `${x - 1}px`, border: `1px solid ${color}`, "background-color": color, - opacity: this.props.active ? "opacity:1 !important" : undefined, }); } } @@ -27034,7 +26361,7 @@ argToFocus: Number, }; - const functions = functionRegistry.content; + const functions$2 = functionRegistry.content; const ASSISTANT_WIDTH = 300; const AUTOCOMPLETE_ENTRIES = 10; const selectionIndicatorClass = "selector-flag"; @@ -27595,10 +26922,10 @@ const parentFunction = tokenContext?.parent.toUpperCase(); if (tokenContext && parentFunction && - parentFunction in functions && + parentFunction in functions$2 && token.type !== "UNKNOWN") { // initialize Formula Assistant - const description = functions[parentFunction]; + const description = functions$2[parentFunction]; const argPosition = tokenContext.argPosition; this.functionDescriptionState.functionName = parentFunction; this.functionDescriptionState.functionDescription = description; @@ -33922,6 +33249,145 @@ } } + var State; + (function (State) { + /** + * Initial state. + * Expecting any reference for the left part of a range + * e.g. "A1", "1", "A", "Sheet1!A1", "Sheet1!A" + */ + State[State["LeftRef"] = 0] = "LeftRef"; + /** + * Expecting any reference for the right part of a range + * e.g. "A1", "1", "A", "Sheet1!A1", "Sheet1!A" + */ + State[State["RightRef"] = 1] = "RightRef"; + /** + * Expecting the separator without any constraint on the right part + */ + State[State["Separator"] = 2] = "Separator"; + /** + * Expecting the separator for a full column range + */ + State[State["FullColumnSeparator"] = 3] = "FullColumnSeparator"; + /** + * Expecting the separator for a full row range + */ + State[State["FullRowSeparator"] = 4] = "FullRowSeparator"; + /** + * Expecting the right part of a full column range + * e.g. "1", "A1" + */ + State[State["RightColumnRef"] = 5] = "RightColumnRef"; + /** + * Expecting the right part of a full row range + * e.g. "A", "A1" + */ + State[State["RightRowRef"] = 6] = "RightRowRef"; + /** + * Final state. A range has been matched + */ + State[State["Found"] = 7] = "Found"; + })(State || (State = {})); + const goTo = (state, guard = () => true) => [ + { + goTo: state, + guard, + }, + ]; + const goToMulti = (state, guard = () => true) => ({ + goTo: state, + guard, + }); + const machine = { + [State.LeftRef]: { + REFERENCE: goTo(State.Separator), + NUMBER: goTo(State.FullRowSeparator), + SYMBOL: [ + goToMulti(State.FullColumnSeparator, (token) => isColReference(token.value)), + goToMulti(State.FullRowSeparator, (token) => isRowReference(token.value)), + ], + }, + [State.FullColumnSeparator]: { + SPACE: goTo(State.FullColumnSeparator), + OPERATOR: goTo(State.RightColumnRef, (token) => token.value === ":"), + }, + [State.FullRowSeparator]: { + SPACE: goTo(State.FullRowSeparator), + OPERATOR: goTo(State.RightRowRef, (token) => token.value === ":"), + }, + [State.Separator]: { + SPACE: goTo(State.Separator), + OPERATOR: goTo(State.RightRef, (token) => token.value === ":"), + }, + [State.RightRef]: { + SPACE: goTo(State.RightRef), + NUMBER: goTo(State.Found), + REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)), + SYMBOL: goTo(State.Found, (token) => isColHeader(token.value)), + }, + [State.RightColumnRef]: { + SPACE: goTo(State.RightColumnRef), + SYMBOL: goTo(State.Found, (token) => isColHeader(token.value)), + REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)), + }, + [State.RightRowRef]: { + SPACE: goTo(State.RightRowRef), + NUMBER: goTo(State.Found), + REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)), + }, + [State.Found]: {}, + }; + /** + * Check if the list of tokens starts with a sequence of tokens representing + * a range. + * If a range is found, the sequence is removed from the list and is returned + * as a single token. + */ + function matchReference(tokens) { + let head = 0; + let transitions = machine[State.LeftRef]; + const matchedTokens = []; + while (transitions !== undefined) { + const token = tokens[head++]; + if (!token) { + return null; + } + const transition = transitions[token.type]?.find((transition) => transition.guard(token)); + const nextState = transition ? transition.goTo : undefined; + switch (nextState) { + case undefined: + return null; + case State.Found: + matchedTokens.push(token); + tokens.splice(0, head); + return { + type: "REFERENCE", + value: concat(matchedTokens.map((token) => token.value)), + }; + default: + transitions = machine[nextState]; + matchedTokens.push(token); + break; + } + } + return null; + } + /** + * Take the result of the tokenizer and transform it to be usable in the + * manipulations of range + * + * @param formula + */ + function rangeTokenize(formula, locale = DEFAULT_LOCALE) { + const tokens = tokenize(formula, locale); + const result = []; + while (tokens.length) { + result.push(matchReference(tokens) || tokens.shift()); + } + return result; + } + /** * parses a formula (as a string) into the same formula, * but with the references to other cells extracted @@ -35081,6 +34547,751 @@ } } + class FunctionCodeBuilder { + scope; + code = ""; + constructor(scope = new Scope()) { + this.scope = scope; + } + append(...lines) { + this.code += lines.map((line) => line.toString()).join("\n") + "\n"; + } + return(expression) { + return new FunctionCodeImpl(this.scope, this.code, expression); + } + toString() { + return indentCode(this.code); + } + } + class FunctionCodeImpl { + scope; + returnExpression; + code; + constructor(scope, code, returnExpression) { + this.scope = scope; + this.returnExpression = returnExpression; + this.code = indentCode(code); + } + toString() { + return this.code; + } + wrapInClosure() { + const closureName = this.scope.nextVariableName(); + const code = new FunctionCodeBuilder(this.scope); + code.append(`const ${closureName} = () => {`); + code.append(this.code); + code.append(`return ${this.returnExpression};`); + code.append(`}`); + return code.return(closureName); + } + assignResultToVariable() { + if (this.scope.isAlreadyDeclared(this.returnExpression)) { + return this; + } + const variableName = this.scope.nextVariableName(); + const code = new FunctionCodeBuilder(this.scope); + code.append(this.code); + code.append(`const ${variableName} = ${this.returnExpression};`); + return code.return(variableName); + } + } + class Scope { + nextId = 1; + declaredVariables = new Set(); + nextVariableName() { + const name = `_${this.nextId++}`; + this.declaredVariables.add(name); + return name; + } + isAlreadyDeclared(name) { + return this.declaredVariables.has(name); + } + } + /** + * Takes a list of strings that might be single or multiline + * and maps them in a list of single line strings. + */ + function splitLines(str) { + return str + .split("\n") + .map((line) => line.trim()) + .filter((line) => line !== ""); + } + function indentCode(code) { + let result = ""; + let indentLevel = 0; + const lines = splitLines(code); + for (const line of lines) { + if (line.startsWith("}")) { + indentLevel--; + } + result += "\t".repeat(indentLevel) + line + "\n"; + if (line.endsWith("{")) { + indentLevel++; + } + } + return result.trim(); + } + + const functionRegex = /[a-zA-Z0-9\_]+(\.[a-zA-Z0-9\_]+)*/; + const UNARY_OPERATORS_PREFIX = ["-", "+"]; + const UNARY_OPERATORS_POSTFIX = ["%"]; + const ASSOCIATIVE_OPERATORS = ["*", "+", "&"]; + const OP_PRIORITY = { + "^": 30, + "%": 30, + "*": 20, + "/": 20, + "+": 15, + "-": 15, + "&": 13, + ">": 10, + "<>": 10, + ">=": 10, + "<": 10, + "<=": 10, + "=": 10, + }; + /** + * Parse the next operand in an arithmetic expression. + * e.g. + * for 1+2*3, the next operand is 1 + * for (1+2)*3, the next operand is (1+2) + * for SUM(1,2)+3, the next operand is SUM(1,2) + */ + function parseOperand(tokens) { + const current = tokens.shift(); + if (!current) { + throw new BadExpressionError(DEFAULT_ERROR_MESSAGE); + } + switch (current.type) { + case "DEBUGGER": + const next = parseExpression(tokens, 1000); + next.debug = true; + return next; + case "NUMBER": + return { type: "NUMBER", value: parseNumber(current.value, DEFAULT_LOCALE) }; + case "STRING": + return { type: "STRING", value: removeStringQuotes(current.value) }; + case "INVALID_REFERENCE": + throw new InvalidReferenceError(); + case "REFERENCE": + if (tokens[0]?.value === ":" && tokens[1]?.type === "REFERENCE") { + tokens.shift(); + const rightReference = tokens.shift(); + return { + type: "REFERENCE", + value: `${current.value}:${rightReference?.value}`, + }; + } + return { + type: "REFERENCE", + value: current.value, + }; + case "SYMBOL": + const value = current.value; + const nextToken = tokens[0]; + if (nextToken?.type === "LEFT_PAREN" && functionRegex.test(current.value)) { + const args = parseFunctionArgs(tokens); + return { type: "FUNCALL", value: value, args }; + } + const upperCaseValue = value.toUpperCase(); + if (upperCaseValue === "TRUE" || upperCaseValue === "FALSE") { + return { type: "BOOLEAN", value: upperCaseValue === "TRUE" }; + } + throw new BadExpressionError(_t("Invalid formula")); + case "LEFT_PAREN": + const result = parseExpression(tokens); + consumeOrThrow(tokens, "RIGHT_PAREN", _t("Missing closing parenthesis")); + return result; + case "OPERATOR": + const operator = current.value; + if (UNARY_OPERATORS_PREFIX.includes(operator)) { + return { + type: "UNARY_OPERATION", + value: operator, + operand: parseExpression(tokens, OP_PRIORITY[operator]), + }; + } + throw new BadExpressionError(_t("Unexpected token: %s", current.value)); + default: + throw new BadExpressionError(_t("Unexpected token: %s", current.value)); + } + } + function parseFunctionArgs(tokens) { + consumeOrThrow(tokens, "LEFT_PAREN", _t("Missing opening parenthesis")); + const nextToken = tokens[0]; + if (nextToken?.type === "RIGHT_PAREN") { + consumeOrThrow(tokens, "RIGHT_PAREN"); + return []; + } + const args = []; + args.push(parseOneFunctionArg(tokens)); + while (tokens[0]?.type !== "RIGHT_PAREN") { + consumeOrThrow(tokens, "ARG_SEPARATOR", _t("Wrong function call")); + args.push(parseOneFunctionArg(tokens)); + } + consumeOrThrow(tokens, "RIGHT_PAREN"); + return args; + } + function parseOneFunctionArg(tokens) { + const nextToken = tokens[0]; + if (nextToken?.type === "ARG_SEPARATOR" || nextToken?.type === "RIGHT_PAREN") { + // arg is empty: "sum(1,,2)" "sum(,1)" "sum(1,)" + return { type: "EMPTY", value: "" }; + } + return parseExpression(tokens); + } + function consumeOrThrow(tokens, type, message = DEFAULT_ERROR_MESSAGE) { + const token = tokens.shift(); + if (!token || token.type !== type) { + throw new BadExpressionError(message); + } + } + function parseExpression(tokens, parent_priority = 0) { + if (tokens.length === 0) { + throw new BadExpressionError(DEFAULT_ERROR_MESSAGE); + } + let left = parseOperand(tokens); + // as long as we have operators with higher priority than the parent one, + // continue parsing the expression because it is a child sub-expression + while (tokens[0]?.type === "OPERATOR" && OP_PRIORITY[tokens[0].value] > parent_priority) { + const operator = tokens.shift().value; + if (UNARY_OPERATORS_POSTFIX.includes(operator)) { + left = { + type: "UNARY_OPERATION", + value: operator, + operand: left, + postfix: true, + }; + } + else { + const right = parseExpression(tokens, OP_PRIORITY[operator]); + left = { + type: "BIN_OPERATION", + value: operator, + left, + right, + }; + } + } + return left; + } + /** + * Parse an expression (as a string) into an AST. + */ + function parse(str) { + return parseTokens(tokenize(str)); + } + function parseTokens(tokens) { + tokens = tokens.filter((x) => x.type !== "SPACE"); + if (tokens[0].value === "=") { + tokens.splice(0, 1); + } + const result = parseExpression(tokens); + if (tokens.length) { + throw new BadExpressionError(DEFAULT_ERROR_MESSAGE); + } + return result; + } + /** + * Allows to visit all nodes of an AST and apply a mapping function + * to nodes of a specific type. + * Useful if you want to convert some part of a formula. + * + * @example + * convertAstNodes(ast, "FUNCALL", convertFormulaToExcel) + * + * function convertFormulaToExcel(ast: ASTFuncall) { + * // ... + * return modifiedAst + * } + */ + function convertAstNodes(ast, type, fn) { + return mapAst(ast, (ast) => { + if (ast.type === type) { + return fn(ast); + } + return ast; + }); + } + function iterateAstNodes(ast) { + return Array.from(astIterator(ast)); + } + function* astIterator(ast) { + yield ast; + switch (ast.type) { + case "FUNCALL": + for (const arg of ast.args) { + yield* astIterator(arg); + } + break; + case "UNARY_OPERATION": + yield* astIterator(ast.operand); + break; + case "BIN_OPERATION": + yield* astIterator(ast.left); + yield* astIterator(ast.right); + break; + } + } + function mapAst(ast, fn) { + ast = fn(ast); + switch (ast.type) { + case "FUNCALL": + return { + ...ast, + args: ast.args.map((child) => mapAst(child, fn)), + }; + case "UNARY_OPERATION": + return { + ...ast, + operand: mapAst(ast.operand, fn), + }; + case "BIN_OPERATION": + return { + ...ast, + right: mapAst(ast.right, fn), + left: mapAst(ast.left, fn), + }; + default: + return ast; + } + } + /** + * Converts an ast formula to the corresponding string + */ + function astToFormula(ast) { + switch (ast.type) { + case "FUNCALL": + const args = ast.args.map((arg) => astToFormula(arg)); + return `${ast.value}(${args.join(",")})`; + case "NUMBER": + return ast.value.toString(); + case "REFERENCE": + return ast.value; + case "STRING": + return `"${ast.value}"`; + case "BOOLEAN": + return ast.value ? "TRUE" : "FALSE"; + case "UNARY_OPERATION": + return ast.postfix + ? leftOperandToFormula(ast) + ast.value + : ast.value + rightOperandToFormula(ast); + case "BIN_OPERATION": + return leftOperandToFormula(ast) + ast.value + rightOperandToFormula(ast); + default: + return ast.value; + } + } + /** + * Convert the left operand of a binary operation to the corresponding string + * and enclose the result inside parenthesis if necessary. + */ + function leftOperandToFormula(operationAST) { + const mainOperator = operationAST.value; + const leftOperation = "left" in operationAST ? operationAST.left : operationAST.operand; + const leftOperator = leftOperation.value; + const needParenthesis = leftOperation.type === "BIN_OPERATION" && OP_PRIORITY[leftOperator] < OP_PRIORITY[mainOperator]; + return needParenthesis ? `(${astToFormula(leftOperation)})` : astToFormula(leftOperation); + } + /** + * Convert the right operand of a binary or unary operation to the corresponding string + * and enclose the result inside parenthesis if necessary. + */ + function rightOperandToFormula(operationAST) { + const mainOperator = operationAST.value; + const rightOperation = "right" in operationAST ? operationAST.right : operationAST.operand; + const rightPriority = OP_PRIORITY[rightOperation.value]; + const mainPriority = OP_PRIORITY[mainOperator]; + let needParenthesis = false; + if (rightOperation.type !== "BIN_OPERATION") { + needParenthesis = false; + } + else if (rightPriority < mainPriority) { + needParenthesis = true; + } + else if (rightPriority === mainPriority && !ASSOCIATIVE_OPERATORS.includes(mainOperator)) { + needParenthesis = true; + } + return needParenthesis ? `(${astToFormula(rightOperation)})` : astToFormula(rightOperation); + } + + const functions$1 = functionRegistry.content; + const OPERATOR_MAP = { + "=": "EQ", + "+": "ADD", + "-": "MINUS", + "*": "MULTIPLY", + "/": "DIVIDE", + ">=": "GTE", + "<>": "NE", + ">": "GT", + "<=": "LTE", + "<": "LT", + "^": "POWER", + "&": "CONCATENATE", + }; + const UNARY_OPERATOR_MAP = { + "-": "UMINUS", + "+": "UPLUS", + "%": "UNARY.PERCENT", + }; + // this cache contains all compiled function code, grouped by "structure". For + // example, "=2*sum(A1:A4)" and "=2*sum(B1:B4)" are compiled into the same + // structural function. + // It is only exported for testing purposes + const functionCache = {}; + // ----------------------------------------------------------------------------- + // COMPILER + // ----------------------------------------------------------------------------- + function compile(formula) { + const tokens = rangeTokenize(formula); + const { dependencies, constantValues } = formulaArguments(tokens); + const cacheKey = compilationCacheKey(tokens, dependencies, constantValues); + if (!functionCache[cacheKey]) { + const ast = parseTokens([...tokens]); + const scope = new Scope(); + if (ast.type === "BIN_OPERATION" && ast.value === ":") { + throw new BadExpressionError(_t("Invalid formula")); + } + if (ast.type === "EMPTY") { + throw new BadExpressionError(_t("Invalid formula")); + } + const compiledAST = compileAST(ast); + const code = new FunctionCodeBuilder(); + code.append(`// ${cacheKey}`); + code.append(compiledAST); + code.append(`return ${compiledAST.returnExpression};`); + let baseFunction = new Function("deps", // the dependencies in the current formula + "ref", // a function to access a certain dependency at a given index + "range", // same as above, but guarantee that the result is in the form of a range + "ctx", code.toString()); + functionCache[cacheKey] = { + // @ts-ignore + execute: baseFunction, + }; + /** + * This function compile the function arguments. It is mostly straightforward, + * except that there is a non trivial transformation in one situation: + * + * If a function argument is asking for a range, and get a cell, we transform + * the cell value into a range. This allow the grid model to differentiate + * between a cell value and a non cell value. + */ + function compileFunctionArgs(ast) { + const { args } = ast; + const functionName = ast.value.toUpperCase(); + const functionDefinition = functions$1[functionName]; + if (!functionDefinition) { + throw new UnknownFunctionError(ast.value); + } + assertEnoughArgs(ast); + const compiledArgs = []; + for (let i = 0; i < args.length; i++) { + const argToFocus = functionDefinition.getArgToFocus(i + 1) - 1; + const argDefinition = functionDefinition.args[argToFocus]; + const currentArg = args[i]; + const argTypes = argDefinition.type || []; + // detect when an argument need to be evaluated as a meta argument + const isMeta = argTypes.includes("META"); + // detect when an argument need to be evaluated as a lazy argument + const isLazy = argDefinition.lazy; + const hasRange = argTypes.some((t) => isRangeType(t)); + const isRangeOnly = argTypes.every((t) => isRangeType(t)); + if (isRangeOnly) { + if (!isRangeInput(currentArg)) { + throw new BadExpressionError(_t("Function %s expects the parameter %s to be reference to a cell or range, not a %s.", functionName, (i + 1).toString(), currentArg.type.toLowerCase())); + } + } + const compiledAST = compileAST(currentArg, isMeta, hasRange, { + functionName, + paramIndex: i + 1, + }); + compiledArgs.push(isLazy ? compiledAST.wrapInClosure() : compiledAST); + } + return compiledArgs; + } + /** + * This function compiles all the information extracted by the parser into an + * executable code for the evaluation of the cells content. It uses a cash to + * not reevaluate identical code structures. + * + * The function is sensitive to parameter “isMeta”. This + * parameter may vary when compiling function arguments: + * isMeta: In some cases the function arguments expects information on the + * cell/range other than the associated value(s). For example the COLUMN + * function needs to receive as argument the coordinates of a cell rather + * than its value. For this we have meta arguments. + */ + function compileAST(ast, isMeta = false, hasRange = false, referenceVerification = {}) { + const code = new FunctionCodeBuilder(scope); + if (ast.type !== "REFERENCE" && !(ast.type === "BIN_OPERATION" && ast.value === ":")) { + if (isMeta) { + throw new BadExpressionError(_t("Argument must be a reference to a cell or range.")); + } + } + if (ast.debug) { + code.append("debugger;"); + } + switch (ast.type) { + case "BOOLEAN": + return code.return(`{ value: ${ast.value} }`); + case "NUMBER": + return code.return(`{ value: this.constantValues.numbers[${constantValues.numbers.indexOf(ast.value)}] }`); + case "STRING": + return code.return(`{ value: this.constantValues.strings[${constantValues.strings.indexOf(ast.value)}] }`); + case "REFERENCE": + const referenceIndex = dependencies.indexOf(ast.value); + if (hasRange) { + return code.return(`range(deps[${referenceIndex}])`); + } + else { + return code.return(`ref(deps[${referenceIndex}], ${isMeta ? "true" : "false"}, "${referenceVerification.functionName || OPERATOR_MAP["="]}", ${referenceVerification.paramIndex})`); + } + case "FUNCALL": + const args = compileFunctionArgs(ast).map((arg) => arg.assignResultToVariable()); + code.append(...args); + const fnName = ast.value.toUpperCase(); + code.append(`ctx.__lastFnCalled = '${fnName}';`); + return code.return(`ctx['${fnName}'](${args.map((arg) => arg.returnExpression)})`); + case "UNARY_OPERATION": { + const fnName = UNARY_OPERATOR_MAP[ast.value]; + const operand = compileAST(ast.operand, false, false, { + functionName: fnName, + }).assignResultToVariable(); + code.append(operand); + code.append(`ctx.__lastFnCalled = '${fnName}';`); + return code.return(`ctx['${fnName}'](${operand.returnExpression})`); + } + case "BIN_OPERATION": { + const fnName = OPERATOR_MAP[ast.value]; + const left = compileAST(ast.left, false, false, { + functionName: fnName, + }).assignResultToVariable(); + const right = compileAST(ast.right, false, false, { + functionName: fnName, + }).assignResultToVariable(); + code.append(left); + code.append(right); + code.append(`ctx.__lastFnCalled = '${fnName}';`); + return code.return(`ctx['${fnName}'](${left.returnExpression}, ${right.returnExpression})`); + } + case "EMPTY": + return code.return("undefined"); + } + } + } + const compiledFormula = { + execute: functionCache[cacheKey].execute, + dependencies, + constantValues, + tokens, + }; + return compiledFormula; + } + /** + * Compute a cache key for the formula. + * References, numbers and strings are replaced with placeholders because + * the compiled formula does not depend on their actual value. + * Both `=A1+1+"2"` and `=A2+2+"3"` are compiled to the exact same function. + * + * Spaces are also ignored to compute the cache key. + * + * A formula `=A1+A2+SUM(2, 2, "2")` have the cache key `=|0|+|1|+SUM(|N0|,|N0|,|S0|)` + */ + function compilationCacheKey(tokens, dependencies, constantValues) { + return concat(tokens.map((token) => { + switch (token.type) { + case "STRING": + const value = removeStringQuotes(token.value); + return `|S${constantValues.strings.indexOf(value)}|`; + case "NUMBER": + return `|N${constantValues.numbers.indexOf(parseNumber(token.value, DEFAULT_LOCALE))}|`; + case "REFERENCE": + case "INVALID_REFERENCE": + return `|${dependencies.indexOf(token.value)}|`; + case "SPACE": + return ""; + default: + return token.value; + } + })); + } + /** + * Return formula arguments which are references, strings and numbers. + */ + function formulaArguments(tokens) { + const constantValues = { + numbers: [], + strings: [], + }; + const dependencies = []; + for (const token of tokens) { + switch (token.type) { + case "INVALID_REFERENCE": + case "REFERENCE": + dependencies.push(token.value); + break; + case "STRING": + const value = removeStringQuotes(token.value); + if (!constantValues.strings.includes(value)) { + constantValues.strings.push(value); + } + break; + case "NUMBER": { + const value = parseNumber(token.value, DEFAULT_LOCALE); + if (!constantValues.numbers.includes(value)) { + constantValues.numbers.push(value); + } + break; + } + } + } + return { + dependencies, + constantValues, + }; + } + /** + * Check if arguments are supplied in the correct quantities + */ + function assertEnoughArgs(ast) { + const nbrArg = ast.args.length; + const functionName = ast.value.toUpperCase(); + const functionDefinition = functions$1[functionName]; + if (nbrArg < functionDefinition.minArgRequired) { + throw new BadExpressionError(_t("Invalid number of arguments for the %s function. Expected %s minimum, but got %s instead.", functionName, functionDefinition.minArgRequired.toString(), nbrArg.toString())); + } + if (nbrArg > functionDefinition.maxArgPossible) { + throw new BadExpressionError(_t("Invalid number of arguments for the %s function. Expected %s maximum, but got %s instead.", functionName, functionDefinition.maxArgPossible.toString(), nbrArg.toString())); + } + const repeatableArgs = functionDefinition.nbrArgRepeating; + if (repeatableArgs > 1) { + const unrepeatableArgs = functionDefinition.args.length - repeatableArgs; + const repeatingArgs = nbrArg - unrepeatableArgs; + if (repeatingArgs % repeatableArgs !== 0) { + throw new BadExpressionError(_t("Invalid number of arguments for the %s function. Expected all arguments after position %s to be supplied by groups of %s arguments", functionName, unrepeatableArgs.toString(), repeatableArgs.toString())); + } + } + } + function isRangeType(type) { + return type.startsWith("RANGE"); + } + function isRangeInput(arg) { + if (arg.type === "REFERENCE") { + return true; + } + if (arg.type === "FUNCALL") { + const fnDef = functions$1[arg.value.toUpperCase()]; + return fnDef && isRangeType(fnDef.returns[0]); + } + return false; + } + + /** + * Add the following information on tokens: + * - length + * - start + * - end + */ + function enrichTokens(tokens) { + let current = 0; + return tokens.map((x) => { + const len = x.value.toString().length; + const token = Object.assign({}, x, { + start: current, + end: current + len, + length: len, + }); + current = token.end; + return token; + }); + } + /** + * add on each token the length, start and end + * also matches the opening to its closing parenthesis (using the same number) + */ + function mapParenthesis(tokens) { + let maxParen = 1; + const stack = []; + return tokens.map((token) => { + if (token.type === "LEFT_PAREN") { + stack.push(maxParen); + token.parenIndex = maxParen; + maxParen++; + } + else if (token.type === "RIGHT_PAREN") { + token.parenIndex = stack.pop(); + } + return token; + }); + } + /** + * add on each token its parent function and the index corresponding to + * its position as an argument of the function. + * In this example "=MIN(42,SUM(MAX(1,2),3))": + * - the parent function of the token correspond to number 42 is the MIN function + * - the argument position of the token correspond to number 42 is 0 + * - the parent function of the token correspond to number 3 is the SUM function + * - the argument position of the token correspond to number 3 is 1 + */ + function mapParentFunction(tokens) { + let stack = []; + let functionStarted = ""; + const res = tokens.map((token, i) => { + if (!["SPACE", "LEFT_PAREN"].includes(token.type)) { + functionStarted = ""; + } + switch (token.type) { + case "SYMBOL": + functionStarted = token.value; + break; + case "LEFT_PAREN": + stack.push({ parent: functionStarted, argPosition: 0 }); + functionStarted = ""; + break; + case "RIGHT_PAREN": + stack.pop(); + break; + case "ARG_SEPARATOR": + if (stack.length) { + // increment position on current function + stack[stack.length - 1].argPosition++; + } + break; + } + if (stack.length) { + const functionContext = stack[stack.length - 1]; + if (functionContext.parent) { + token.functionContext = Object.assign({}, functionContext); + } + } + return token; + }); + return res; + } + /** + * Take the result of the tokenizer and transform it to be usable in the composer. + * + * @param formula + */ + function composerTokenize(formula, locale) { + const tokens = rangeTokenize(formula, locale); + return mapParentFunction(mapParenthesis(enrichTokens(tokens))); + } + + const functions = functionRegistry.content; + function isExportableToExcel(tokens) { + try { + const nonExportableFunctions = iterateAstNodes(parseTokens(tokens)).filter((ast) => ast.type === "FUNCALL" && !functions[ast.value.toUpperCase()]?.isExported); + return nonExportableFunctions.length === 0; + } + catch (error) { + return false; + } + } + /** * Core Plugin * @@ -39400,13 +39611,41 @@ handle(cmd) { switch (cmd.type) { case "UPDATE_LOCALE": - this.history.update("locale", cmd.locale); + const oldLocale = this.locale; + const newLocale = cmd.locale; + this.history.update("locale", newLocale); + this.changeCellsDateFormatWithLocale(oldLocale, newLocale); break; } } getLocale() { return this.locale; } + changeCellsDateFormatWithLocale(oldLocale, newLocale) { + for (const sheetId of this.getters.getSheetIds()) { + for (const [cellId, cell] of Object.entries(this.getters.getCells(sheetId))) { + let formatToApply; + if (cell.format === oldLocale.dateFormat) { + formatToApply = newLocale.dateFormat; + } + if (cell.format === oldLocale.timeFormat) { + formatToApply = newLocale.timeFormat; + } + if (cell.format === getDateTimeFormat(oldLocale)) { + formatToApply = getDateTimeFormat(newLocale); + } + if (formatToApply) { + const { col, row, sheetId } = this.getters.getCellPosition(cellId); + this.dispatch("UPDATE_CELL", { + col, + row, + sheetId, + format: formatToApply, + }); + } + } + } + } import(data) { this.locale = data.settings?.locale ?? DEFAULT_LOCALE; } @@ -39612,9 +39851,9 @@ */ getCellsDependingOn(positionIds) { const visited = new JetSet(); - const queue = Array.from(positionIds); + const queue = Array.from(positionIds).reverse(); while (queue.length > 0) { - const node = queue.shift(); + const node = queue.pop(); visited.add(node); const adjacentNodes = this.inverseDependencies.get(node) || new Set(); for (const adjacentNode of adjacentNodes) { @@ -40762,7 +41001,7 @@ return percentile(rangeValues, Number(threshold.value) / 100, true); case "formula": const value = threshold.value && this.getters.evaluateFormula(sheetId, threshold.value); - return !(value instanceof Promise) ? value : null; + return typeof value === "number" ? value : null; default: return null; } @@ -40888,10 +41127,13 @@ } const [value0, value1] = rule.values.map((val) => { if (val.startsWith("=")) { - return this.getters.evaluateFormula(target.sheetId, val); + return this.getters.evaluateFormula(target.sheetId, val) ?? ""; } return parseLiteral(val, DEFAULT_LOCALE); }); + if (isMatrix(value0) || isMatrix(value1)) { + return false; + } switch (rule.operator) { case "IsEmpty": return cell.value.toString().trim() === ""; @@ -41080,7 +41322,7 @@ const formula = compile(value); const translatedFormula = this.getters.getTranslatedCellFormula(sheetId, offset.col, offset.row, formula, formula.dependencies.map((d) => this.getters.getRangeFromSheetXC(sheetId, d))); const evaluated = this.getters.evaluateFormula(sheetId, translatedFormula); - return evaluated ? evaluated.toString() : ""; + return evaluated && !isMatrix(evaluated) ? evaluated.toString() : ""; } catch (e) { return e instanceof EvaluationError ? e.errorType : CellErrorType.GenericError; @@ -41089,8 +41331,8 @@ } } - class UIRowSizePlugin extends UIPlugin { - static getters = ["getRowSize"]; + class HeaderSizeUIPlugin extends UIPlugin { + static getters = ["getRowSize", "getHeaderSize"]; tallestCellInRow = {}; ctx = document.createElement("canvas").getContext("2d"); handle(cmd) { @@ -41169,6 +41411,14 @@ this.tallestCellInRow[sheetId][row]?.size ?? DEFAULT_CELL_HEIGHT); } + getHeaderSize(sheetId, dimension, index) { + if (this.getters.isHeaderHidden(sheetId, dimension, index)) { + return 0; + } + return dimension === "ROW" + ? this.getRowSize(sheetId, index) + : this.getters.getColSize(sheetId, index); + } updateRowSizeForCellChange(sheetId, row, col) { const tallestCellInRow = this.tallestCellInRow[sheetId]?.[row]; if (tallestCellInRow?.cell.col === col) { @@ -44891,6 +45141,11 @@ return "MaximumRangesReached" /* CommandResult.MaximumRangesReached */; } break; + case "REMOVE_RANGE": + if (this.ranges.length === 1) { + return "MinimumRangesReached" /* CommandResult.MinimumRangesReached */; + } + break; case "CHANGE_RANGE": if (this.inputHasSingleRange && cmd.value.split(",").length > 1) { return "MaximumRangesReached" /* CommandResult.MaximumRangesReached */; @@ -45574,9 +45829,6 @@ "getTextWidth", "getCellText", "getCellMultiLineText", - "getColDimensions", - "getRowDimensions", - "getColRowOffset", ]; ctx = document.createElement("canvas").getContext("2d"); // --------------------------------------------------------------------------- @@ -45661,55 +45913,6 @@ const text = this.getters.getCellText(position, this.getters.shouldShowFormulas()); return splitTextToWidth(this.ctx, text, style, width); } - /** - * Returns the size, start and end coordinates of a column on an unfolded sheet - */ - getColDimensions(sheetId, col) { - const start = this.getColRowOffset("COL", 0, col, sheetId); - const size = this.getters.getColSize(sheetId, col); - const isColHidden = this.getters.isColHidden(sheetId, col); - return { - start, - size, - end: start + (isColHidden ? 0 : size), - }; - } - /** - * Returns the size, start and end coordinates of a row an unfolded sheet - */ - getRowDimensions(sheetId, row) { - const start = this.getColRowOffset("ROW", 0, row, sheetId); - const size = this.getters.getRowSize(sheetId, row); - const isRowHidden = this.getters.isRowHidden(sheetId, row); - return { - start, - size: size, - end: start + (isRowHidden ? 0 : size), - }; - } - /** - * Returns the offset of a header (determined by the dimension) at the given index - * based on the referenceIndex given. If start === 0, this method will return - * the start attribute of the header. - * - * i.e. The size from A to B is the distance between A.start and B.end - */ - getColRowOffset(dimension, referenceIndex, index, sheetId = this.getters.getActiveSheetId()) { - if (index < referenceIndex) { - return -this.getColRowOffset(dimension, index, referenceIndex); - } - let offset = 0; - for (let i = referenceIndex; i < index; i++) { - if (this.getters.isHeaderHidden(sheetId, dimension, i)) { - continue; - } - offset += - dimension === "COL" - ? this.getters.getColSize(sheetId, i) - : this.getters.getRowSize(sheetId, i); - } - return offset; - } doesCellHaveGridIcon(position) { const isFilterHeader = this.getters.isFilterHeader(position); const hasListIcon = !this.getters.isReadonly() && this.getters.cellHasListDataValidationIcon(position); @@ -47308,7 +47511,7 @@ // display a simplified and parsable string otherwise const timeFormat = Number.isInteger(value) ? locale.dateFormat - : locale.dateFormat + " " + locale.timeFormat; + : getDateTimeFormat(locale); return formatValue(value, { locale, format: timeFormat }); } return this.numberComposerContent(value, format, locale); @@ -47575,6 +47778,9 @@ const cellValue = content.startsWith("=") ? this.getters.evaluateFormula(this.sheetId, content) : parseLiteral(content, this.getters.getLocale()); + if (isMatrix(cellValue)) { + return "Success" /* CommandResult.Success */; + } const validationResult = this.getters.getValidationResultForCellValue(cellValue, cellPosition); if (!validationResult.isValid && validationResult.rule.isBlocking) { return "BlockingValidationRule" /* CommandResult.BlockingValidationRule */; @@ -47950,10 +48156,7 @@ switch (cmd.type) { case "START": const firstSheetId = this.getters.getVisibleSheetIds()[0]; - this.dispatch("ACTIVATE_SHEET", { - sheetIdTo: firstSheetId, - sheetIdFrom: firstSheetId, - }); + this.activateSheet(firstSheetId, firstSheetId); const { col, row } = this.getters.getNextVisibleCellPosition({ sheetId: firstSheetId, col: 0, @@ -47966,25 +48169,7 @@ this.moveClient({ sheetId: firstSheetId, col: 0, row: 0 }); break; case "ACTIVATE_SHEET": { - if (!this.getters.isSheetVisible(cmd.sheetIdTo)) { - this.dispatch("SHOW_SHEET", { sheetId: cmd.sheetIdTo }); - } - this.setActiveSheet(cmd.sheetIdTo); - this.sheetsData[cmd.sheetIdFrom] = { - gridSelection: deepCopy(this.gridSelection), - }; - if (cmd.sheetIdTo in this.sheetsData) { - Object.assign(this, this.sheetsData[cmd.sheetIdTo]); - this.selection.resetDefaultAnchor(this, deepCopy(this.gridSelection.anchor)); - } - else { - const { col, row } = this.getters.getNextVisibleCellPosition({ - sheetId: cmd.sheetIdTo, - col: 0, - row: 0, - }); - this.selectCell(col, row); - } + this.activateSheet(cmd.sheetIdFrom, cmd.sheetIdTo); break; } case "REMOVE_COLUMNS_ROWS": { @@ -48251,6 +48436,27 @@ // --------------------------------------------------------------------------- // Other // --------------------------------------------------------------------------- + activateSheet(sheetIdFrom, sheetIdTo) { + if (!this.getters.isSheetVisible(sheetIdTo)) { + this.dispatch("SHOW_SHEET", { sheetId: sheetIdTo }); + } + this.setActiveSheet(sheetIdTo); + this.sheetsData[sheetIdFrom] = { + gridSelection: deepCopy(this.gridSelection), + }; + if (sheetIdTo in this.sheetsData) { + Object.assign(this, this.sheetsData[sheetIdTo]); + this.selection.resetDefaultAnchor(this, deepCopy(this.gridSelection.anchor)); + } + else { + const { col, row } = this.getters.getNextVisibleCellPosition({ + sheetId: sheetIdTo, + col: 0, + row: 0, + }); + this.selectCell(col, row); + } + } /** * Ensure selections are not outside sheet boundaries. * They are clipped to fit inside the sheet if needed. @@ -48362,9 +48568,7 @@ const toRemove = isBasedBefore ? cmd.elements.map((el) => el + thickness) : cmd.elements; let currentIndex = cmd.base; for (const element of toRemove) { - const size = cmd.dimension === "COL" - ? this.getters.getColSize(cmd.sheetId, element) - : this.getters.getRowSize(cmd.sheetId, element); + const size = this.getters.getHeaderSize(cmd.sheetId, cmd.dimension, element); this.dispatch("RESIZE_COLUMNS_ROWS", { dimension: cmd.dimension, sheetId: cmd.sheetId, @@ -48545,22 +48749,22 @@ * visible cell. * It returns -1 if no column is found. */ - getColIndex(x, absolute = false) { + getColIndex(x) { if (x < this.offsetCorrectionX || x > this.offsetCorrectionX + this.viewportWidth) { return -1; } - return this.searchHeaderIndex("COL", x - this.offsetCorrectionX, this.left, absolute); + return this.searchHeaderIndex("COL", x - this.offsetCorrectionX, this.left); } /** * Return the index of a row given an offset y, based on the pane top * visible cell. * It returns -1 if no row is found. */ - getRowIndex(y, absolute = false) { + getRowIndex(y) { if (y < this.offsetCorrectionY || y > this.offsetCorrectionY + this.viewportHeight) { return -1; } - return this.searchHeaderIndex("ROW", y - this.offsetCorrectionY, this.top, absolute); + return this.searchHeaderIndex("ROW", y - this.offsetCorrectionY, this.top); } /** * This function will make sure that the provided cell position (or current selected position) is part of @@ -48591,7 +48795,7 @@ finalTarget++; } const finalTargetEnd = this.getters.getColDimensions(sheetId, finalTarget).end; - const startIndex = this.searchHeaderIndex("COL", finalTargetEnd - this.viewportWidth - this.offsetCorrectionX, this.boundaries.left, true); + const startIndex = this.searchHeaderIndex("COL", finalTargetEnd - this.viewportWidth - this.offsetCorrectionX, this.boundaries.left); this.offsetX = this.getters.getColDimensions(sheetId, startIndex).end - this.offsetCorrectionX; this.offsetScrollbarX = this.offsetX; @@ -48618,7 +48822,7 @@ finalTarget++; } const finalTargetEnd = this.getters.getRowDimensions(sheetId, finalTarget).end; - const startIndex = this.searchHeaderIndex("ROW", finalTargetEnd - this.viewportHeight - this.offsetCorrectionY, this.boundaries.top, true); + const startIndex = this.searchHeaderIndex("ROW", finalTargetEnd - this.viewportHeight - this.offsetCorrectionY, this.boundaries.top); this.offsetY = this.getters.getRowDimensions(sheetId, startIndex).end - this.offsetCorrectionY; this.offsetScrollbarY = this.offsetY; @@ -48674,23 +48878,24 @@ !this.getters.isRowHidden(this.sheetId, row)); } // PRIVATE - searchHeaderIndex(dimension, position, startIndex = 0, absolute = false) { - let size = 0; + searchHeaderIndex(dimension, position, startIndex = 0) { const sheetId = this.sheetId; const headers = this.getters.getNumberHeaders(sheetId, dimension); - for (let i = startIndex; i <= headers - 1; i++) { - const isHiddenInViewport = !absolute && dimension === "COL" - ? i < this.left && i > this.right - : i < this.top && i > this.bottom; - if (this.getters.isHeaderHidden(sheetId, dimension, i) || isHiddenInViewport) { - continue; + // using a binary search: + let start = startIndex; + let end = headers; + while (start <= end && start !== headers && end !== -1) { + const mid = Math.floor((start + end) / 2); + const offset = this.getters.getColRowOffset(dimension, startIndex, mid); + const size = this.getters.getHeaderSize(sheetId, dimension, mid); + if (position >= offset && position < offset + size) { + return mid; } - size += - dimension === "COL" - ? this.getters.getColSize(sheetId, i) - : this.getters.getRowSize(sheetId, i); - if (size > position) { - return i; + else if (position >= offset + size) { + start = mid + 1; + } + else { + end = mid - 1; } } return -1; @@ -48722,8 +48927,8 @@ this.offsetScrollbarX = Math.max(0, viewportWidth - this.viewportWidth); } } - this.left = this.getColIndex(this.offsetScrollbarX, true); - this.right = this.getColIndex(this.offsetScrollbarX + this.viewportWidth, true); + this.left = this.getColIndex(this.offsetScrollbarX); + this.right = this.getColIndex(this.offsetScrollbarX + this.viewportWidth); if (this.right === -1) { this.right = this.boundaries.right; } @@ -48739,8 +48944,8 @@ this.offsetScrollbarY = Math.max(0, paneHeight - this.viewportHeight); } } - this.top = this.getRowIndex(this.offsetScrollbarY, true); - this.bottom = this.getRowIndex(this.offsetScrollbarY + this.viewportHeight, true); + this.top = this.getRowIndex(this.offsetScrollbarY); + this.bottom = this.getRowIndex(this.offsetScrollbarY + this.viewportHeight); if (this.bottom === -1) { this.bottom = this.boundaries.bottom; } @@ -48855,6 +49060,7 @@ gridOffsetX = 0; gridOffsetY = 0; sheetsWithDirtyViewports = new Set(); + shouldAdjustViewports = false; // --------------------------------------------------------------------------- // Command Handling // --------------------------------------------------------------------------- @@ -48886,11 +49092,19 @@ const sheetId = this.getters.getActiveSheetId(); col = Math.min(col, this.getters.getNumberCols(sheetId) - 1); row = Math.min(row, this.getters.getNumberRows(sheetId) - 1); - this.refreshViewport(this.getters.getActiveSheetId(), { col, row }); + if (!this.sheetsWithDirtyViewports.has(sheetId)) { + this.refreshViewport(this.getters.getActiveSheetId(), { col, row }); + } } } handle(cmd) { this.cleanViewports(); + // changing the evaluation can hide/show rows because of data filters + if (invalidateEvaluationCommands.has(cmd.type)) { + for (const sheetId of this.getters.getSheetIds()) { + this.sheetsWithDirtyViewports.add(sheetId); + } + } switch (cmd.type) { case "START": this.selection.observe(this, { @@ -48900,7 +49114,10 @@ break; case "UNDO": case "REDO": - this.resetSheetViews(); + for (const sheetId of this.getters.getSheetIds()) { + this.sheetsWithDirtyViewports.add(sheetId); + } + this.shouldAdjustViewports = true; break; case "RESIZE_SHEETVIEW": this.resizeSheetView(cmd.height, cmd.width, cmd.gridOffsetX, cmd.gridOffsetY); @@ -48921,12 +49138,17 @@ this.shiftVertically(shiftedOffsetY); break; } + case "REMOVE_FILTER_TABLE": + case "UPDATE_FILTER": + this.sheetsWithDirtyViewports.add(cmd.sheetId); + break; case "REMOVE_COLUMNS_ROWS": case "RESIZE_COLUMNS_ROWS": case "HIDE_COLUMNS_ROWS": case "ADD_COLUMNS_ROWS": case "UNHIDE_COLUMNS_ROWS": - case "UPDATE_FILTER": + case "UNGROUP_HEADERS": + case "GROUP_HEADERS": case "FOLD_HEADER_GROUP": case "UNFOLD_HEADER_GROUP": case "FOLD_HEADER_GROUPS_IN_ZONE": @@ -48934,18 +49156,19 @@ case "UNFOLD_ALL_HEADER_GROUPS": case "FOLD_ALL_HEADER_GROUPS": { const sheetId = "sheetId" in cmd ? cmd.sheetId : this.getters.getActiveSheetId(); - this.resetViewports(sheetId); + this.sheetsWithDirtyViewports.add(sheetId); break; } case "UPDATE_CELL": // update cell content or format can change hidden rows because of data filters if ("content" in cmd || "format" in cmd || cmd.style?.fontSize !== undefined) { - this.sheetsWithDirtyViewports.add(cmd.sheetId); + for (const sheetId of this.getters.getSheetIds()) { + this.sheetsWithDirtyViewports.add(sheetId); + } } break; case "ACTIVATE_SHEET": - this.setViewports(); - this.refreshViewport(cmd.sheetIdTo); + this.sheetsWithDirtyViewports.add(cmd.sheetIdTo); break; case "UNFREEZE_ROWS": case "UNFREEZE_COLUMNS": @@ -48966,8 +49189,16 @@ finalize() { for (const sheetId of this.sheetsWithDirtyViewports) { this.resetViewports(sheetId); + if (this.shouldAdjustViewports) { + const position = this.getters.getSheetPosition(sheetId); + const viewports = this.getSubViewports(sheetId); + Object.values(viewports).forEach((viewport) => { + viewport.adjustPosition(position); + }); + } } this.sheetsWithDirtyViewports = new Set(); + this.shouldAdjustViewports = false; this.setViewports(); } setViewports() { @@ -49085,10 +49316,7 @@ if (!visibleIndexes.includes(i)) { continue; } - offset += - dimension === "COL" - ? this.getters.getColSize(sheetId, i) - : this.getters.getRowSize(sheetId, i); + offset += this.getters.getHeaderSize(sheetId, dimension, i); } return offset; } @@ -49261,16 +49489,6 @@ } } } - resetSheetViews() { - for (let sheetId of Object.keys(this.viewports)) { - const position = this.getters.getSheetPosition(sheetId); - this.resetViewports(sheetId); - const viewports = this.getSubViewports(sheetId); - Object.values(viewports).forEach((viewport) => { - viewport.adjustPosition(position); - }); - } - } resizeSheetView(height, width, gridOffsetX = 0, gridOffsetY = 0) { this.sheetViewHeight = height; this.sheetViewWidth = width; @@ -49407,6 +49625,120 @@ } } + class HeaderPositionsUIPlugin extends UIPlugin { + static getters = ["getColDimensions", "getRowDimensions", "getColRowOffset"]; + headerPositions = {}; + isDirty = true; + handle(cmd) { + if (invalidateEvaluationCommands.has(cmd.type)) { + this.headerPositions = {}; + this.isDirty = true; + } + switch (cmd.type) { + case "START": + for (const sheetId of this.getters.getSheetIds()) { + this.headerPositions[sheetId] = this.computeHeaderPositionsOfSheet(sheetId); + } + break; + case "UPDATE_CELL": + if ("content" in cmd || "format" in cmd || cmd.style?.fontSize !== undefined) { + this.headerPositions = {}; + this.isDirty = true; + } + break; + case "UPDATE_FILTER": + case "REMOVE_FILTER_TABLE": + this.headerPositions = {}; + this.isDirty = true; + break; + case "REMOVE_COLUMNS_ROWS": + case "RESIZE_COLUMNS_ROWS": + case "HIDE_COLUMNS_ROWS": + case "ADD_COLUMNS_ROWS": + case "UNHIDE_COLUMNS_ROWS": + case "FOLD_HEADER_GROUP": + case "UNFOLD_HEADER_GROUP": + case "FOLD_HEADER_GROUPS_IN_ZONE": + case "UNFOLD_HEADER_GROUPS_IN_ZONE": + case "UNFOLD_ALL_HEADER_GROUPS": + case "FOLD_ALL_HEADER_GROUPS": + case "UNGROUP_HEADERS": + case "GROUP_HEADERS": + case "CREATE_SHEET": + this.headerPositions[cmd.sheetId] = this.computeHeaderPositionsOfSheet(cmd.sheetId); + break; + case "DUPLICATE_SHEET": + this.headerPositions[cmd.sheetIdTo] = deepCopy(this.headerPositions[cmd.sheetId]); + break; + } + } + finalize() { + if (this.isDirty) { + for (const sheetId of this.getters.getSheetIds()) { + this.headerPositions[sheetId] = this.computeHeaderPositionsOfSheet(sheetId); + } + this.isDirty = false; + } + } + /** + * Returns the size, start and end coordinates of a column on an unfolded sheet + */ + getColDimensions(sheetId, col) { + const start = this.headerPositions[sheetId]["COL"][col]; + const size = this.getters.getColSize(sheetId, col); + const isColHidden = this.getters.isColHidden(sheetId, col); + return { + start, + size, + end: start + (isColHidden ? 0 : size), + }; + } + /** + * Returns the size, start and end coordinates of a row an unfolded sheet + */ + getRowDimensions(sheetId, row) { + const start = this.headerPositions[sheetId]["ROW"][row]; + const size = this.getters.getRowSize(sheetId, row); + const isRowHidden = this.getters.isRowHidden(sheetId, row); + return { + start, + size: size, + end: start + (isRowHidden ? 0 : size), + }; + } + /** + * Returns the offset of a header (determined by the dimension) at the given index + * based on the referenceIndex given. If start === 0, this method will return + * the start attribute of the header. + * + * i.e. The size from A to B is the distance between A.start and B.end + */ + getColRowOffset(dimension, referenceIndex, index, sheetId = this.getters.getActiveSheetId()) { + const referencePosition = this.headerPositions[sheetId][dimension][referenceIndex]; + const position = this.headerPositions[sheetId][dimension][index]; + return position - referencePosition; + } + computeHeaderPositionsOfSheet(sheetId) { + return { + COL: this.computePositions(sheetId, "COL"), + ROW: this.computePositions(sheetId, "ROW"), + }; + } + computePositions(sheetId, dimension) { + const positions = {}; + let offset = 0; + // loop on number of headers +1 so the position of (last header + 1) is the end of the sheet + for (let i = 0; i < this.getters.getNumberHeaders(sheetId, dimension) + 1; i++) { + positions[i] = offset; + if (this.getters.isHeaderHidden(sheetId, dimension, i)) { + continue; + } + offset += this.getters.getHeaderSize(sheetId, dimension, i); + } + return positions; + } + } + const corePluginRegistry = new Registry() .add("settings", SettingsPlugin) .add("sheet", SheetPlugin) @@ -49425,7 +49757,6 @@ // Plugins which handle a specific feature, without handling any core commands const featurePluginRegistry = new Registry() .add("ui_sheet", SheetUIPlugin) - .add("header_visibility_ui", HeaderVisibilityUIPlugin) .add("ui_options", UIOptionsPlugin) .add("selectionInputManager", SelectionInputsManagerPlugin) .add("highlight", HighlightPlugin) @@ -49444,6 +49775,8 @@ const statefulUIPluginRegistry = new Registry() .add("selection", GridSelectionPlugin) .add("evaluation_filter", FilterEvaluationPlugin) + .add("header_visibility_ui", HeaderVisibilityUIPlugin) + .add("header_positions", HeaderPositionsUIPlugin) .add("viewport", SheetViewPlugin) .add("clipboard", ClipboardPlugin) .add("edition", EditionPlugin); @@ -49452,7 +49785,7 @@ .add("evaluation", EvaluationPlugin) .add("evaluation_chart", EvaluationChartPlugin) .add("evaluation_cf", EvaluationConditionalFormatPlugin) - .add("row_size", UIRowSizePlugin) + .add("row_size", HeaderSizeUIPlugin) .add("custom_colors", CustomColorsPlugin) .add("data_validation_ui", EvaluationDataValidationPlugin); @@ -50743,7 +51076,11 @@ static template = "o-spreadsheet-ActionButton"; actionButton = createAction(this.props.action); setup() { - owl.onWillUpdateProps(() => (this.actionButton = createAction(this.props.action))); + owl.onWillUpdateProps((nextProps) => { + if (nextProps.action !== this.props.action) { + this.actionButton = createAction(this.props.action); + } + }); } get isVisible() { return this.actionButton.isVisible(this.env); @@ -50814,6 +51151,7 @@ css /* scss */ ` .o-border-selector { padding: 4px; + background-color: white; } .o-divider { border-right: 1px solid #e0e2e4; @@ -51174,6 +51512,7 @@ line-height: 1.2; font-size: 13px; font-weight: 500; + background-color: #fff; .o-topbar-top { border-bottom: 1px solid ${SEPARATOR_COLOR}; @@ -52623,9 +52962,9 @@ * Apply the changes of the given HistoryChange to the state */ function applyChange(change, target) { - let val = change.root; - let key = change.path[change.path.length - 1]; - for (let pathIndex = 0; pathIndex < change.path.slice(0, -1).length; pathIndex++) { + let val = change.path[0]; + const key = change.path.at(-1); + for (let pathIndex = 1; pathIndex < change.path.slice(0, -1).length; pathIndex++) { const p = change.path[pathIndex]; if (val[p] === undefined) { const nextPath = change.path[pathIndex + 1]; @@ -53305,13 +53644,14 @@ } addChange(...args) { const val = args.pop(); - const [root, ...path] = args; + const root = args[0]; let value = root; - let key = path[path.length - 1]; - for (let pathIndex = 0; pathIndex <= path.length - 2; pathIndex++) { - const p = path[pathIndex]; + let key = args.at(-1); + const pathLength = args.length - 2; + for (let pathIndex = 1; pathIndex <= pathLength; pathIndex++) { + const p = args[pathIndex]; if (value[p] === undefined) { - const nextPath = path[pathIndex + 1]; + const nextPath = args[pathIndex + 1]; value[p] = createEmptyStructure(nextPath); } value = value[p]; @@ -53320,8 +53660,7 @@ return; } this.changes.push({ - root, - path, + path: args, before: value[key], after: val, }); @@ -54104,14 +54443,14 @@ */ function figureCoordinates(headers, position) { let currentPosition = 0; - for (const [headerIndex, header] of Object.entries(headers)) { + for (const [headerIndex, header] of headers.entries()) { if (currentPosition <= position && position < currentPosition + header.size) { return { - index: parseInt(headerIndex), + index: headerIndex, offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_WIDTH), }; } - else { + else if (headerIndex < headers.length - 1) { currentPosition += header.size; } } @@ -55530,9 +55869,9 @@ exports.tokenize = tokenize; - __info__.version = "16.5.0-alpha.12"; - __info__.date = "2023-10-20T15:39:44.984Z"; - __info__.hash = "430f0907"; + __info__.version = "17.0.1"; + __info__.date = "2023-11-02T09:48:14.706Z"; + __info__.hash = "0887d5bb5"; })(this.o_spreadsheet = this.o_spreadsheet || {}, owl); diff --git a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml index 417f82eaf46..83c939246d8 100644 --- a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml +++ b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml @@ -1,9 +1,9 @@ @@ -66,7 +66,7 @@ Border Color
@@ -300,9 +300,7 @@ -
-
-
+
@@ -1312,7 +1310,7 @@
Text Color Fill Color