diff --git a/docs/datatypes/matrices.md b/docs/datatypes/matrices.md index 965ed0e068..ab02879bd7 100644 --- a/docs/datatypes/matrices.md +++ b/docs/datatypes/matrices.md @@ -331,6 +331,49 @@ method `.set()`, the matrix will be resized. By default, new items will be initialized with zero, but it is possible to specify an alternative value using the optional third argument `defaultValue`. +## Advanced Indexing + +Boolean array indexing is a technique that allows you to filter, replace, and set values in an array based on logical conditions. This can be done by creating a boolean array that represents the desired conditions, and then using that array as an index to select the elements of the original array that meet those conditions. + +For example, a boolean array can be created to represent all the even numbers in an array, and then used to filter the original array to only include the even numbers. Alternatively, a boolean array can be created to represent all the elements of an array that are greater than a certain value, and then used to replace all the elements of the original array that are greater than that value with a new value. + + +```js +const q = [1, 2, 3, 4] +math.subset(q, math.index([true, false, true, false])) // Array [1, 3] + +// filtering +math.subset(q, math.index(math.larger(q, 2))) // Array [3, 4] + +// filtering with no matches +math.subset(q, math.index(math.larger(q, 5))) // Array [] + +// setting specific values, please note that the replacement value is broadcasted +q = math.subset(q, math.index(math.smaller(q, 3)), 0) // q = [0, 0, 3, 4] + +// replacing specific values +math.subset(q, math.index(math.equal(q, 0)), [1, 2]) // q = [1, 2, 3, 4] +``` + +The same can be accomplished in the parser in a much more compact manner. Please note that everything after `#` are comments. +```js +math.evaluate(` +q = [1, 2, 3, 4] +q[[true, false, true, false]] # Matrix [1, 3] +q[q>2] # Matrix [3, 4] +q[q>5] # Matrix [] +q[q <3] = 0 # q = [0, 0, 3, 4] +q[q==0] = [1, 2] # q = [1, 2, 3, 4] +`) +``` +The expression inside the index can be as complex as needed as long it evaluates to an array of booleans of the same size. +```js +math.evaluate(` +q = [1, 2, 3, 4] +r = [6, 5, 4, 3] +q[q > 3 and r < 4] # [4] +`) +``` ## Iterating diff --git a/src/expression/embeddedDocs/construction/index.js b/src/expression/embeddedDocs/construction/index.js index 076ca3455b..b055cb8f2b 100644 --- a/src/expression/embeddedDocs/construction/index.js +++ b/src/expression/embeddedDocs/construction/index.js @@ -12,11 +12,12 @@ export const indexDocs = { description: 'Create an index to get or replace a subset of a matrix', examples: [ - '[1, 2, 3]', 'A = [1, 2, 3; 4, 5, 6]', 'A[1, :]', 'A[1, 2] = 50', - 'A[1:2, 1:2] = ones(2, 2)' + 'A[1:2, 1:2] = 1', + 'B = [1, 2, 3]', + 'B[B>1 and B<3]' ], seealso: [ 'bignumber', 'boolean', 'complex', 'matrix,', 'number', 'range', 'string', 'unit' diff --git a/src/expression/transform/index.transform.js b/src/expression/transform/index.transform.js index 1d6ea8eabd..94b6672966 100644 --- a/src/expression/transform/index.transform.js +++ b/src/expression/transform/index.transform.js @@ -2,9 +2,9 @@ import { isArray, isBigNumber, isMatrix, isNumber, isRange } from '../../utils/i import { factory } from '../../utils/factory.js' const name = 'index' -const dependencies = ['Index'] +const dependencies = ['Index', 'getMatrixDataType'] -export const createIndexTransform = /* #__PURE__ */ factory(name, dependencies, ({ Index }) => { +export const createIndexTransform = /* #__PURE__ */ factory(name, dependencies, ({ Index, getMatrixDataType }) => { /** * Attach a transform function to math.index * Adds a property transform containing the transform function. @@ -16,20 +16,22 @@ export const createIndexTransform = /* #__PURE__ */ factory(name, dependencies, for (let i = 0, ii = arguments.length; i < ii; i++) { let arg = arguments[i] - // change from one-based to zero based, and convert BigNumber to number + // change from one-based to zero based, convert BigNumber to number and leave Array of Booleans as is if (isRange(arg)) { arg.start-- arg.end -= (arg.step > 0 ? 0 : 2) } else if (arg && arg.isSet === true) { arg = arg.map(function (v) { return v - 1 }) } else if (isArray(arg) || isMatrix(arg)) { - arg = arg.map(function (v) { return v - 1 }) + if (getMatrixDataType(arg) !== 'boolean') { + arg = arg.map(function (v) { return v - 1 }) + } } else if (isNumber(arg)) { arg-- } else if (isBigNumber(arg)) { arg = arg.toNumber() - 1 } else if (typeof arg === 'string') { - // leave as is + // leave as is } else { throw new TypeError('Dimension must be an Array, Matrix, number, string, or Range') } diff --git a/src/expression/transform/subset.transform.js b/src/expression/transform/subset.transform.js index 84a089dd76..50e1f41755 100644 --- a/src/expression/transform/subset.transform.js +++ b/src/expression/transform/subset.transform.js @@ -3,10 +3,10 @@ import { errorTransform } from './utils/errorTransform.js' import { createSubset } from '../../function/matrix/subset.js' const name = 'subset' -const dependencies = ['typed', 'matrix'] +const dependencies = ['typed', 'matrix', 'zeros', 'add'] -export const createSubsetTransform = /* #__PURE__ */ factory(name, dependencies, ({ typed, matrix }) => { - const subset = createSubset({ typed, matrix }) +export const createSubsetTransform = /* #__PURE__ */ factory(name, dependencies, ({ typed, matrix, zeros, add }) => { + const subset = createSubset({ typed, matrix, zeros, add }) /** * Attach a transform function to math.subset diff --git a/src/function/matrix/concat.js b/src/function/matrix/concat.js index 8db4bd6668..6d8fe0f44b 100644 --- a/src/function/matrix/concat.js +++ b/src/function/matrix/concat.js @@ -1,6 +1,6 @@ import { isBigNumber, isMatrix, isNumber } from '../../utils/is.js' import { clone } from '../../utils/object.js' -import { arraySize } from '../../utils/array.js' +import { arraySize, concat as _concat } from '../../utils/array.js' import { IndexError } from '../../error/IndexError.js' import { DimensionError } from '../../error/DimensionError.js' import { factory } from '../../utils/factory.js' @@ -94,7 +94,7 @@ export const createConcat = /* #__PURE__ */ factory(name, dependencies, ({ typed let res = matrices.shift() while (matrices.length) { - res = _concat(res, matrices.shift(), dim, 0) + res = _concat(res, matrices.shift(), dim) } return asMatrix ? matrix(res) : res @@ -105,31 +105,3 @@ export const createConcat = /* #__PURE__ */ factory(name, dependencies, ({ typed } }) }) - -/** - * Recursively concatenate two matrices. - * The contents of the matrices is not cloned. - * @param {Array} a Multi dimensional array - * @param {Array} b Multi dimensional array - * @param {number} concatDim The dimension on which to concatenate (zero-based) - * @param {number} dim The current dim (zero-based) - * @return {Array} c The concatenated matrix - * @private - */ -function _concat (a, b, concatDim, dim) { - if (dim < concatDim) { - // recurse into next dimension - if (a.length !== b.length) { - throw new DimensionError(a.length, b.length) - } - - const c = [] - for (let i = 0; i < a.length; i++) { - c[i] = _concat(a[i], b[i], concatDim, dim + 1) - } - return c - } else { - // concatenate this dimension - return a.concat(b) - } -} diff --git a/src/function/matrix/subset.js b/src/function/matrix/subset.js index 5e088e64b4..48d7571315 100644 --- a/src/function/matrix/subset.js +++ b/src/function/matrix/subset.js @@ -1,14 +1,14 @@ import { isIndex } from '../../utils/is.js' import { clone } from '../../utils/object.js' -import { validateIndex } from '../../utils/array.js' +import { isEmptyIndex, validateIndex, validateIndexSourceSize } from '../../utils/array.js' import { getSafeProperty, setSafeProperty } from '../../utils/customs.js' import { DimensionError } from '../../error/DimensionError.js' import { factory } from '../../utils/factory.js' const name = 'subset' -const dependencies = ['typed', 'matrix'] +const dependencies = ['typed', 'matrix', 'zeros', 'add'] -export const createSubset = /* #__PURE__ */ factory(name, dependencies, ({ typed, matrix }) => { +export const createSubset = /* #__PURE__ */ factory(name, dependencies, ({ typed, matrix, zeros, add }) => { /** * Get or set a subset of a matrix or string. * @@ -20,13 +20,15 @@ export const createSubset = /* #__PURE__ */ factory(name, dependencies, ({ typed * * // get a subset * const d = [[1, 2], [3, 4]] - * math.subset(d, math.index(1, 0)) // returns 3 - * math.subset(d, math.index([0, 1], 1)) // returns [[2], [4]] + * math.subset(d, math.index(1, 0)) // returns 3 + * math.subset(d, math.index([0, 1], 1)) // returns [[2], [4]] + * math.subset(d, math.index([false, true], 0)) // returns [[3]] * * // replace a subset * const e = [] - * const f = math.subset(e, math.index(0, [0, 2]), [5, 6]) // f = [[5, 6]] - * const g = math.subset(f, math.index(1, 1), 7, 0) // g = [[5, 6], [0, 7]] + * const f = math.subset(e, math.index(0, [0, 2]), [5, 6]) // f = [[5, 0, 6]] + * const g = math.subset(f, math.index(1, 1), 7, 0) // g = [[5, 0, 6], [0, 7, 0]] + * math.subset(g, math.index([false, true], 1), 8) // returns [[5, 0, 6], [0, 8, 0]] * * // get submatrix using ranges * const M = [ @@ -34,7 +36,7 @@ export const createSubset = /* #__PURE__ */ factory(name, dependencies, ({ typed * [4,5,6], * [7,8,9] * ] - * math.subset(M, math.index(math.range(0,2), math.range(0,3))) // [[1,2,3],[4,5,6]] + * math.subset(M, math.index(math.range(0,2), math.range(0,3))) // [[1, 2, 3], [4, 5, 6]] * * See also: * @@ -53,49 +55,81 @@ export const createSubset = /* #__PURE__ */ factory(name, dependencies, ({ typed * math.matrix elements will be left undefined. * @return {Array | Matrix | string} Either the retrieved subset or the updated matrix. */ + return typed(name, { // get subset - 'Array, Index': function (value, index) { - const m = matrix(value) - const subset = m.subset(index) // returns a Matrix - return index.isScalar() - ? subset - : subset.valueOf() // return an Array (like the input) - }, - 'Matrix, Index': function (value, index) { + if (isEmptyIndex(index)) { return matrix() } + validateIndexSourceSize(value, index) return value.subset(index) }, + 'Array, Index': typed.referTo('Matrix, Index', function (subsetRef) { + return function (value, index) { + const subsetResult = subsetRef(matrix(value), index) + return index.isScalar() ? subsetResult : subsetResult.valueOf() + } + }), + 'Object, Index': _getObjectProperty, 'string, Index': _getSubstring, // set subset - 'Array, Index, any': function (value, index, replacement) { - return matrix(clone(value)) - .subset(index, replacement, undefined) - .valueOf() + 'Matrix, Index, any, any': function (value, index, replacement, defaultValue) { + if (isEmptyIndex(index)) { return value } + validateIndexSourceSize(value, index) + return value.clone().subset(index, _broadcastReplacement(replacement, index), defaultValue) }, - 'Array, Index, any, any': function (value, index, replacement, defaultValue) { - return matrix(clone(value)) - .subset(index, replacement, defaultValue) - .valueOf() - }, + 'Array, Index, any, any': typed.referTo('Matrix, Index, any, any', function (subsetRef) { + return function (value, index, replacement, defaultValue) { + const subsetResult = subsetRef(matrix(value), index, replacement, defaultValue) + return subsetResult.isMatrix ? subsetResult.valueOf() : subsetResult + } + }), - 'Matrix, Index, any': function (value, index, replacement) { - return value.clone().subset(index, replacement) - }, + 'Array, Index, any': typed.referTo('Matrix, Index, any, any', function (subsetRef) { + return function (value, index, replacement) { + return subsetRef(matrix(value), index, replacement, undefined).valueOf() + } + }), - 'Matrix, Index, any, any': function (value, index, replacement, defaultValue) { - return value.clone().subset(index, replacement, defaultValue) - }, + 'Matrix, Index, any': typed.referTo('Matrix, Index, any, any', function (subsetRef) { + return function (value, index, replacement) { return subsetRef(value, index, replacement, undefined) } + }), 'string, Index, string': _setSubstring, 'string, Index, string, string': _setSubstring, 'Object, Index, any': _setObjectProperty }) + + /** + * Broadcasts a replacment value to be the same size as index + * @param {number | BigNumber | Array | Matrix} replacement Replacement value to try to broadcast + * @param {*} index Index value + * @returns broadcasted replacement that matches the size of index + */ + + function _broadcastReplacement (replacement, index) { + if (typeof replacement === 'string') { + throw new Error('can\'t boradcast a string') + } + if (index._isScalar) { + return replacement + } + + const indexSize = index.size() + if (indexSize.every(d => d > 0)) { + try { + return add(replacement, zeros(indexSize)) + } catch (error) { + return replacement + } + } else { + return replacement + } + } }) /** @@ -110,6 +144,10 @@ function _getSubstring (str, index) { // TODO: better error message throw new TypeError('Index expected') } + + if (isEmptyIndex(index)) { return '' } + validateIndexSourceSize(Array.from(str), index) + if (index.size().length !== 1) { throw new DimensionError(index.size().length, 1) } @@ -134,7 +172,7 @@ function _getSubstring (str, index) { * @param {string} str string to be replaced * @param {Index} index An index or list of indices (character positions) * @param {string} replacement Replacement string - * @param {string} [defaultValue] Default value to be uses when resizing + * @param {string} [defaultValue] Default value to be used when resizing * the string. is ' ' by default * @returns {string} result * @private @@ -144,6 +182,8 @@ function _setSubstring (str, index, replacement, defaultValue) { // TODO: better error message throw new TypeError('Index expected') } + if (isEmptyIndex(index)) { return str } + validateIndexSourceSize(Array.from(str), index) if (index.size().length !== 1) { throw new DimensionError(index.size().length, 1) } @@ -197,6 +237,8 @@ function _setSubstring (str, index, replacement, defaultValue) { * @private */ function _getObjectProperty (object, index) { + if (isEmptyIndex(index)) { return undefined } + if (index.size().length !== 1) { throw new DimensionError(index.size(), 1) } @@ -218,6 +260,7 @@ function _getObjectProperty (object, index) { * @private */ function _setObjectProperty (object, index, replacement) { + if (isEmptyIndex(index)) { return object } if (index.size().length !== 1) { throw new DimensionError(index.size(), 1) } diff --git a/src/type/matrix/DenseMatrix.js b/src/type/matrix/DenseMatrix.js index 027cf697fe..c27b59aab2 100644 --- a/src/type/matrix/DenseMatrix.js +++ b/src/type/matrix/DenseMatrix.js @@ -1,5 +1,5 @@ import { isArray, isBigNumber, isCollection, isIndex, isMatrix, isNumber, isString, typeOf } from '../../utils/is.js' -import { arraySize, getArrayDataType, processSizesWildcard, reshape, resize, unsqueeze, validate, validateIndex } from '../../utils/array.js' +import { arraySize, getArrayDataType, processSizesWildcard, reshape, resize, unsqueeze, validate, validateIndex, broadcastTo } from '../../utils/array.js' import { format } from '../../utils/string.js' import { isInteger } from '../../utils/number.js' import { clone, deepStrictEqual } from '../../utils/object.js' @@ -321,11 +321,23 @@ export const createDenseMatrixClass = /* #__PURE__ */ factory(name, dependencies if (sSize.length !== 0) { throw new TypeError('Scalar expected') } - matrix.set(index.min(), submatrix, defaultValue) } else { // set a submatrix + // broadcast submatrix + if (!deepStrictEqual(sSize, iSize)) { + try { + if (sSize.length === 0) { + submatrix = broadcastTo([submatrix], iSize) + } else { + submatrix = broadcastTo(submatrix, iSize) + } + sSize = arraySize(submatrix) + } catch { + } + } + // validate dimensions if (iSize.length < matrix._size.length) { throw new DimensionError(iSize.length, matrix._size.length, '<') diff --git a/src/type/matrix/MatrixIndex.js b/src/type/matrix/MatrixIndex.js index f1b234dcf0..000ce3466c 100644 --- a/src/type/matrix/MatrixIndex.js +++ b/src/type/matrix/MatrixIndex.js @@ -1,12 +1,12 @@ -import { isMatrix, isRange } from '../../utils/is.js' +import { isArray, isMatrix, isRange } from '../../utils/is.js' import { clone } from '../../utils/object.js' import { isInteger } from '../../utils/number.js' import { factory } from '../../utils/factory.js' const name = 'Index' -const dependencies = ['ImmutableDenseMatrix'] +const dependencies = ['ImmutableDenseMatrix', 'getMatrixDataType'] -export const createIndexClass = /* #__PURE__ */ factory(name, dependencies, ({ ImmutableDenseMatrix }) => { +export const createIndexClass = /* #__PURE__ */ factory(name, dependencies, ({ ImmutableDenseMatrix, getMatrixDataType }) => { /** * Create an index. An Index can store ranges and sets for multiple dimensions. * Matrix.get, Matrix.set, and math.subset accept an Index as input. @@ -19,7 +19,9 @@ export const createIndexClass = /* #__PURE__ */ factory(name, dependencies, ({ I * A string (containing a name of an object property) * An instance of Range * An Array with the Set values + * An Array with Booleans * A Matrix with the Set values + * A Matrix with Booleans * * The parameters start, end, and step must be integer numbers. * @@ -33,22 +35,34 @@ export const createIndexClass = /* #__PURE__ */ factory(name, dependencies, ({ I } this._dimensions = [] + this._sourceSize = [] this._isScalar = true for (let i = 0, ii = arguments.length; i < ii; i++) { const arg = arguments[i] - + const argIsArray = isArray(arg) + const argIsMatrix = isMatrix(arg) + let sourceSize = null if (isRange(arg)) { this._dimensions.push(arg) this._isScalar = false - } else if (Array.isArray(arg) || isMatrix(arg)) { + } else if (argIsArray || argIsMatrix) { // create matrix - const m = _createImmutableMatrix(arg.valueOf()) + let m + + if (getMatrixDataType(arg) === 'boolean') { + if (argIsArray) m = _createImmutableMatrix(_booleansArrayToNumbersForIndex(arg).valueOf()) + if (argIsMatrix) m = _createImmutableMatrix(_booleansArrayToNumbersForIndex(arg._data).valueOf()) + sourceSize = arg.valueOf().length + } else { + m = _createImmutableMatrix(arg.valueOf()) + } + this._dimensions.push(m) // size const size = m.size() // scalar - if (size.length !== 1 || size[0] !== 1) { + if (size.length !== 1 || size[0] !== 1 || sourceSize !== null) { this._isScalar = false } } else if (typeof arg === 'number') { @@ -59,6 +73,7 @@ export const createIndexClass = /* #__PURE__ */ factory(name, dependencies, ({ I } else { throw new TypeError('Dimension must be an Array, Matrix, number, string, or Range') } + this._sourceSize.push(sourceSize) // TODO: implement support for wildcard '*' } } @@ -89,6 +104,7 @@ export const createIndexClass = /* #__PURE__ */ factory(name, dependencies, ({ I const index = new Index() index._dimensions = clone(this._dimensions) index._isScalar = this._isScalar + index._sourceSize = this._sourceSize return index } @@ -274,3 +290,19 @@ export const createIndexClass = /* #__PURE__ */ factory(name, dependencies, ({ I return Index }, { isClass: true }) + +/** + * Receives an array of booleans and returns an array of Numbers for Index + * @param {Array} booleanArrayIndex An array of booleans + * @return {Array} A set of numbers ready for index + */ +function _booleansArrayToNumbersForIndex (booleanArrayIndex) { + // gets an array of booleans and returns an array of numbers + const indexOfNumbers = [] + booleanArrayIndex.forEach((bool, idx) => { + if (bool) { + indexOfNumbers.push(idx) + } + }) + return indexOfNumbers +} diff --git a/src/type/matrix/function/index.js b/src/type/matrix/function/index.js index 45e50c4fc4..a5540527f0 100644 --- a/src/type/matrix/function/index.js +++ b/src/type/matrix/function/index.js @@ -1,5 +1,5 @@ +import { isBigNumber, isMatrix, isArray } from '../../../utils/is.js' import { factory } from '../../../utils/factory.js' -import { isBigNumber, isMatrix } from '../../../utils/is.js' const name = 'index' const dependencies = ['typed', 'Index'] @@ -19,17 +19,19 @@ export const createIndex = /* #__PURE__ */ factory(name, dependencies, ({ typed, * - A number * - A string for getting/setting an object property * - An instance of `Range` - * - A one-dimensional Array or a Matrix with numbers + * - A one-dimensional Array or a Matrix with numbers or booleans * * Indexes must be zero-based, integer numbers. * * Examples: * * const b = [1, 2, 3, 4, 5] - * math.subset(b, math.index([1, 2, 3])) // returns [2, 3, 4] + * math.subset(b, math.index([1, 2, 3])) // returns [2, 3, 4] + * math.subset(b, math.index([false, true, true, true, false])) // returns [2, 3, 4] * * const a = math.matrix([[1, 2], [3, 4]]) * a.subset(math.index(0, 1)) // returns 2 + * a.subset(math.index(0, [false, true])) // returns 2 * * See also: * @@ -43,7 +45,7 @@ export const createIndex = /* #__PURE__ */ factory(name, dependencies, ({ typed, const ranges = args.map(function (arg) { if (isBigNumber(arg)) { return arg.toNumber() // convert BigNumber to Number - } else if (Array.isArray(arg) || isMatrix(arg)) { + } else if (isArray(arg) || isMatrix(arg)) { return arg.map(function (elem) { // convert BigNumber to Number return isBigNumber(elem) ? elem.toNumber() : elem diff --git a/src/type/matrix/utils/broadcast.js b/src/type/matrix/utils/broadcast.js index 4b89888585..2d1da91198 100644 --- a/src/type/matrix/utils/broadcast.js +++ b/src/type/matrix/utils/broadcast.js @@ -1,3 +1,4 @@ +import { checkBroadcastingRules } from '../../../utils/array.js' import { factory } from '../../../utils/factory.js' const name = 'broadcast' @@ -36,10 +37,8 @@ export const createBroadcast = /* #__PURE__ */ factory( } // check if the broadcasting rules applyes for both matrices - for (let dim = 0; dim < N; dim++) { - _checkRules(sizeA, sizeMax, dim) - _checkRules(sizeB, sizeMax, dim) - } + checkBroadcastingRules(sizeA, sizeMax) + checkBroadcastingRules(sizeB, sizeMax) // reshape A or B if needed to make them ready for concat let AA = A.clone() @@ -69,9 +68,5 @@ export const createBroadcast = /* #__PURE__ */ factory( // stretches a matrix up to a certain size in a certain dimension return concat(...Array(sizeToStretch).fill(arrayToStretch), dimToStretch) } - - function _checkRules (shape, sizeMax, dim) { - if ((shape[dim] < sizeMax[dim]) & (shape[dim] > 1)) { throw new Error(`shape missmatch: missmatch is found in arg with shape (${shape}) not possible to broadcast dimension ${dim} with size ${shape[dim]} to size ${sizeMax[dim]}`) } - } } ) diff --git a/src/utils/array.js b/src/utils/array.js index cd434ab63b..c510245a7b 100644 --- a/src/utils/array.js +++ b/src/utils/array.js @@ -1,8 +1,9 @@ import { isInteger } from './number.js' -import { isNumber } from './is.js' +import { isNumber, isBigNumber, isArray, isString } from './is.js' import { format } from './string.js' import { DimensionError } from '../error/DimensionError.js' import { IndexError } from '../error/IndexError.js' +import { deepStrictEqual } from './object.js' /** * Calculate the size of a multi dimensional array. @@ -79,6 +80,21 @@ export function validate (array, size) { } } +/** + * Validate whether the source of the index matches the size of the Array + * @param {Array | Matrix} array Array to be validated + * @param {Index} index Index with the source information to validate + * @throws DimensionError + */ +export function validateIndexSourceSize (value, index) { + const valueSize = value.isMatrix ? value._size : arraySize(value) + const sourceSize = index._sourceSize + // checks if the source size is not null and matches the valueSize + sourceSize.forEach((sourceDim, i) => { + if (sourceDim !== null && sourceDim !== valueSize[i]) { throw new DimensionError(sourceDim, valueSize[i]) } + }) +} + /** * Test whether index is an integer number with index >= 0 and index < length * when length is provided @@ -86,18 +102,44 @@ export function validate (array, size) { * @param {number} [length] Length of the array */ export function validateIndex (index, length) { - if (!isNumber(index) || !isInteger(index)) { - throw new TypeError('Index must be an integer (value: ' + index + ')') + if (index !== undefined) { + if (!isNumber(index) || !isInteger(index)) { + throw new TypeError('Index must be an integer (value: ' + index + ')') + } + if (index < 0 || (typeof length === 'number' && index >= length)) { + throw new IndexError(index, length) + } } - if (index < 0 || (typeof length === 'number' && index >= length)) { - throw new IndexError(index, length) +} + +/** + * Test if and index has empty values + * @param {number} index Zero-based index + */ +export function isEmptyIndex (index) { + for (let i = 0; i < index._dimensions.length; ++i) { + const dimension = index._dimensions[i] + if (dimension._data && isArray(dimension._data)) { + if (dimension._size[0] === 0) { + return true + } + } else if (dimension.isRange) { + if (dimension.start === dimension.end) { + return true + } + } else if (isString(dimension)) { + if (dimension.length === 0) { + return true + } + } } + return false } /** * Resize a multi dimensional array. The resized array is returned. - * @param {Array} array Array to be resized - * @param {Array.} size Array with the size of each dimension + * @param {Array | number} array Array to be resized + * @param {number[]} size Array with the size of each dimension * @param {*} [defaultValue=0] Value to be filled in in new entries, * zero by default. Specify for example `null`, * to clearly see entries that are not explicitly @@ -105,10 +147,8 @@ export function validateIndex (index, length) { * @return {Array} array The resized array */ export function resize (array, size, defaultValue) { - // TODO: add support for scalars, having size=[] ? - // check the type of the arguments - if (!Array.isArray(array) || !Array.isArray(size)) { + if (!Array.isArray(size)) { throw new TypeError('Array expected') } if (size.length === 0) { @@ -123,6 +163,11 @@ export function resize (array, size, defaultValue) { } }) + // convert number to an array + if (isNumber(array) || isBigNumber(array)) { + array = [array] + } + // recursively resize the array const _defaultValue = (defaultValue !== undefined) ? defaultValue : 0 _resize(array, size, 0, _defaultValue) @@ -193,7 +238,7 @@ function _resize (array, size, dim, defaultValue) { /** * Re-shape a multi dimensional array to fit the specified dimensions * @param {Array} array Array to be reshaped - * @param {Array.} sizes List of sizes for each dimension + * @param {number[]} sizes List of sizes for each dimension * @returns {Array} Array whose data has been formatted to fit the * specified dimensions * @@ -238,10 +283,10 @@ export function reshape (array, sizes) { /** * Replaces the wildcard -1 in the sizes array. - * @param {Array.} sizes List of sizes for each dimension. At most on wildcard. + * @param {number[]} sizes List of sizes for each dimension. At most on wildcard. * @param {number} currentLength Number of elements in the array. * @throws {Error} If more than one wildcard or unable to replace it. - * @returns {Array.} The sizes array with wildcard replaced. + * @returns {number[]} The sizes array with wildcard replaced. */ export function processSizesWildcard (sizes, currentLength) { const newLength = product(sizes) @@ -269,7 +314,7 @@ export function processSizesWildcard (sizes, currentLength) { /** * Computes the product of all array elements. - * @param {Array} array Array of factors + * @param {number[]} array Array of factors * @returns {number} Product of all elements */ function product (array) { @@ -279,7 +324,7 @@ function product (array) { /** * Iteratively re-shape a multi dimensional array to fit the specified dimensions * @param {Array} array Array to be reshaped - * @param {Array.} sizes List of sizes for each dimension + * @param {number[]} sizes List of sizes for each dimension * @returns {Array} Array whose data has been formatted to fit the * specified dimensions */ @@ -589,7 +634,7 @@ export function getArrayDataType (array, typeOf) { /** * Return the last item from an array - * @param array + * @param {array} * @returns {*} */ export function last (array) { @@ -598,6 +643,8 @@ export function last (array) { /** * Get all but the last element of array. + * @param {array} + * @returns {*} */ export function initial (array) { return array.slice(0, array.length - 1) @@ -612,3 +659,167 @@ export function initial (array) { export function contains (array, item) { return array.indexOf(item) !== -1 } + +/** + * Recursively concatenate two matrices. + * The contents of the matrices is not cloned. + * @param {Array} a Multi dimensional array + * @param {Array} b Multi dimensional array + * @param {number} concatDim The dimension on which to concatenate (zero-based) + * @param {number} dim The current dim (zero-based) + * @return {Array} c The concatenated matrix + * @private + */ +function concatRecursive (a, b, concatDim, dim) { + if (dim < concatDim) { + // recurse into next dimension + if (a.length !== b.length) { + throw new DimensionError(a.length, b.length) + } + + const c = [] + for (let i = 0; i < a.length; i++) { + c[i] = concatRecursive(a[i], b[i], concatDim, dim + 1) + } + return c + } else { + // concatenate this dimension + return a.concat(b) + } +} + +/** + * Concatenates many arrays in the specified direction + * @param {...Array} arrays All the arrays to concatenate + * @param {number} concatDim The dimension on which to concatenate (zero-based) + * @returns +*/ +export function concat () { + const arrays = Array.prototype.slice.call(arguments, 0, -1) + const concatDim = Array.prototype.slice.call(arguments, -1) + + if (arrays.length === 1) { + return arrays[0] + } + if (arrays.length > 1) { + return arrays.slice(1).reduce(function (A, B) { return concatRecursive(A, B, concatDim, 0) }, arrays[0]) + } else { + throw new Error('Wrong number of arguments in function concat') + } +} + +/** + * Receives two or more sizes and get's the broadcasted size for both. + * @param {...number[]} sizes Sizes to broadcast together + * @returns + */ +export function broadcastSizes (...sizes) { + const dimensions = sizes.map((s) => s.length) + const N = Math.max(...dimensions) + const sizeMax = new Array(N).fill(null) + // check for every size + for (let i = 0; i < sizes.length; i++) { + const size = sizes[i] + const dim = dimensions[i] + for (let j = 0; j < dim; j++) { + const n = N - dim + j + if (size[j] > sizeMax[n]) { + sizeMax[n] = size[j] + } + } + } + for (let i = 0; i < sizes.length; i++) { + checkBroadcastingRules(sizes[i], sizeMax) + } + return sizeMax +} + +/** + * Checks if it's possible to broadcast a size to another size + * @param {number[]} size The size of the array to check + * @param {number[]} toSize The size of the array to validate if it can be broadcasted to + */ +export function checkBroadcastingRules (size, toSize) { + const N = toSize.length + const dim = size.length + for (let j = 0; j < dim; j++) { + const n = N - dim + j + if ((size[j] < toSize[n] && size[j] > 1) || (size[j] > toSize[n])) { + throw new Error( + `shape missmatch: missmatch is found in arg with shape (${size}) not possible to broadcast dimension ${dim} with size ${size[j]} to size ${toSize[n]}` + ) + } + } +} + +/** + * Broadcasts a single array to a certain size + * @param {array} array Array to be broadcasted + * @param {number[]} toSize Size to broadcast the array + * @returns The broadcasted array + */ +export function broadcastTo (array, toSize) { + let Asize = arraySize(array) + if (deepStrictEqual(Asize, toSize)) { + return array + } + checkBroadcastingRules(Asize, toSize) + const broadcastedSize = broadcastSizes(Asize, toSize) + const N = broadcastedSize.length + const paddedSize = [...Array(N - Asize.length).fill(1), ...Asize] + + let A = clone(array) + // reshape A if needed to make it ready for concat + if (Asize.length < N) { + A = reshape(A, paddedSize) + Asize = arraySize(A) + } + + // stretches the array on each dimension to make it the same size as index + for (let dim = 0; dim < N; dim++) { + if (Asize[dim] < broadcastedSize[dim]) { + A = stretch(A, broadcastedSize[dim], dim) + Asize = arraySize(A) + } + } + return A +} + +/** + * Broadcasts arrays and returns the broadcasted arrays in an array + * @param {...Array | any} arrays + * @returns + */ +export function broadcastArrays (...arrays) { + if (arrays.length === 0) { + throw new Error('Insuficient number of argumnets in function broadcastArrays') + } + if (arrays.length === 1) { + return arrays[0] + } + const sizes = arrays.map(function (array) { return arraySize(array) }) + const broadcastedSize = broadcastSizes(...sizes) + const broadcastedArrays = [] + arrays.forEach(function (array) { broadcastedArrays.push(broadcastTo(array, broadcastedSize)) }) + return broadcastedArrays +} + +/** + * stretches a matrix up to a certain size in a certain dimension + * @param {Array} arrayToStretch + * @param {number[]} sizeToStretch + * @param {number} dimToStretch + * @returns + */ +export function stretch (arrayToStretch, sizeToStretch, dimToStretch) { + return concat(...Array(sizeToStretch).fill(arrayToStretch), dimToStretch) +} + +/** + * Deep clones a multidimensional array + * @param {Array} array + * @returns cloned array + */ +export function clone (array) { + return Object.assign([], array) +} diff --git a/test/unit-tests/expression/parse.test.js b/test/unit-tests/expression/parse.test.js index c2bf5000c2..1380d538d0 100644 --- a/test/unit-tests/expression/parse.test.js +++ b/test/unit-tests/expression/parse.test.js @@ -689,6 +689,17 @@ describe('parse', function () { assert.deepStrictEqual(parseAndEval('a[2:end-1, 2:end-1]', scope), math.matrix([[2, 0], [9, 9]])) }) + it('should get and set broadcasted submatrices in the parser', function () { + const scope = {} + parseAndEval('A = [1, 2, 3, 4]', scope) + assert.deepStrictEqual(parseAndEval('A[A>2]', scope), math.matrix([3, 4])) + parseAndEval('A[A>2] = 20', scope) + assert.deepStrictEqual(scope.A, math.matrix([1, 2, 20, 20])) + parseAndEval('A = [1, 2, 3, 4]', scope) + parseAndEval('A[A > 2] = [15]', scope) + assert.deepStrictEqual(scope.A, math.matrix([1, 2, 15, 15])) + }) + it('should merge nested matrices', function () { const scope = {} parseAndEval('a=[1,2;3,4]', scope) diff --git a/test/unit-tests/function/matrix/subset.test.js b/test/unit-tests/function/matrix/subset.test.js index 88bd76d4c4..10b5003a1e 100644 --- a/test/unit-tests/function/matrix/subset.test.js +++ b/test/unit-tests/function/matrix/subset.test.js @@ -1,5 +1,6 @@ import assert from 'assert' import math from '../../../../src/defaultInstance.js' +import { DimensionError } from '../../../../src/error/DimensionError.js' const subset = math.subset const matrix = math.matrix const Range = math.Range @@ -15,6 +16,44 @@ describe('subset', function () { assert.deepStrictEqual(subset([math.bignumber(2)], index(0)), math.bignumber(2)) }) + it('should get the right subset of an array of booleans', function () { + assert.deepStrictEqual(subset(a, index([true, true], 1)), [[2], [4]]) + assert.deepStrictEqual(subset(a, index([false, true], [true, false])), [[3]]) + assert.deepStrictEqual(subset([math.bignumber(2)], index([true])), [math.bignumber(2)]) + }) + + it('should return an empty value with an empty index', function () { + assert.deepStrictEqual(subset(a, index([], 1)), []) + assert.deepStrictEqual(subset(a, index(new math.Range(0, 0), 1)), []) + assert.deepStrictEqual(subset(b, index([], 1)), math.matrix()) + assert.deepStrictEqual(subset(b, index(new math.Range(0, 0), 1)), math.matrix()) + assert.deepStrictEqual(subset({ a: 1 }, index('')), undefined) + assert.deepStrictEqual(subset('hello', index('')), '') + }) + + it('should get the right subset of an array of booleans in the parser', function () { + assert.deepStrictEqual(math.evaluate('a[[true, true], 2]', { a }), [[2], [4]]) + assert.deepStrictEqual(math.evaluate('a[[false, true], [true, false]]', { a }), [[3]]) + assert.deepStrictEqual(math.evaluate('[bignumber(2)][[true]]'), math.matrix([math.bignumber(2)])) + }) + + it('should throw an error if the array of booleans doesn\'t have the same size as the array', function () { + assert.throws(function () { subset(a, index([true], 0)) }, DimensionError) + assert.throws(function () { subset(a, index([true, true, false], 1)) }, DimensionError) + assert.throws(function () { subset(a, index(0, [true])) }, DimensionError) + assert.throws(function () { subset(a, index(0, [true, true, false])) }, DimensionError) + assert.throws(function () { subset(b, index([true], 0)) }, DimensionError) + assert.throws(function () { subset(b, index([true, true, false], 1)) }, DimensionError) + assert.throws(function () { subset(b, index(0, [true])) }, DimensionError) + assert.throws(function () { subset(b, index(0, [true, true, false])) }, DimensionError) + }) + + it('should return an empty value with an empty index in the parser', function () { + assert.deepStrictEqual(math.evaluate('a[[],1]', { a }), []) + assert.deepStrictEqual(math.evaluate('b[[],1]', { b }), math.matrix()) + // TODO: add test for objects and strings: currently throws no access property when it's "" + }) + it('should throw an error if trying to access an invalid subset of an array', function () { assert.throws(function () { subset(a, index(6, 0)) }, RangeError) assert.throws(function () { subset(a, index(1)) }, RangeError) @@ -32,6 +71,7 @@ describe('subset', function () { const obj = { foo: 'bar' } const i = index('a', 'b') assert.throws(function () { subset(obj, i) }, /DimensionError/) + assert.throws(function () { subset(obj, 'notAnIndex') }, /TypeError.*/) }) it('should get the right subset of a matrix', function () { @@ -68,13 +108,60 @@ describe('subset', function () { assert.deepStrictEqual(subset(d, index(0, 0), 123), [[123, 2], [3, 4]]) }) + it('should leave arrays as such if the index is empty', function () { + assert.deepStrictEqual(subset(d, index([], 1), 1), d) + assert.deepStrictEqual(subset(d, index(1, new Range(0, 0)), 1), d) + assert.deepStrictEqual(subset(d, index([], 1), 1, 1), d) + assert.deepStrictEqual(subset(d, index(1, new Range(0, 0)), 1, 1), d) + assert.deepStrictEqual(subset(g, index([], 1), 1), g) + assert.deepStrictEqual(subset(g, index(1, new Range(0, 0)), 1), g) + assert.deepStrictEqual(subset(g, index([], 1), 1, 1), g) + assert.deepStrictEqual(subset(g, index(1, new Range(0, 0)), 1, 1), g) + assert.deepStrictEqual(subset('hello', index([]), 'x'), 'hello') + assert.deepStrictEqual(subset('hello', index([]), 'x', 'x'), 'hello') + }) + + it('should set the right subset of an array if the replacement can be broadcasted to the index', function () { + assert.deepStrictEqual(d, [[1, 2], [3, 4]]) + assert.deepStrictEqual(subset(d, index(new Range(0, 2), 1), -2), [[1, -2], [3, -2]]) + assert.deepStrictEqual(d, [[1, 2], [3, 4]]) + assert.deepStrictEqual(subset(d, index(2, new Range(0, 2)), [5]), [[1, 2], [3, 4], [5, 5]]) + assert.deepStrictEqual(d, [[1, 2], [3, 4]]) + assert.deepStrictEqual(subset(d, index(0, [0, 1]), 123), [[123, 123], [3, 4]]) + }) + + it('should set the right subset of an array or matrix with default value if the replacement can\'t be broadcasted to the index', function () { + assert.deepStrictEqual(subset(d, index(2, 1), 7, 0), [[1, 2], [3, 4], [0, 7]]) + assert.deepStrictEqual(subset(g, index(2, 1), 7, 0), math.matrix([[1, 2], [3, 4], [0, 7]])) + assert.deepStrictEqual(subset(d, index(1, 2), 7, 0), [[1, 2, 0], [3, 4, 7]]) + assert.deepStrictEqual(subset(g, index(1, 2), 7, 0), math.matrix([[1, 2, 0], [3, 4, 7]])) + }) + it('should set a subset of an array with undefined default value', function () { const a = [] assert.deepStrictEqual(subset(a, index(2), 1), [0, 0, 1]) assert.deepStrictEqual(subset(a, index(2), 1, null), [null, null, 1]) }) - it('should throw an error if setting the subset of an array with an invalid replacement', function () { + it('should set a subset of an array or matrix by broadcasting the replacement', function () { + assert.deepStrictEqual(subset(d, index([0, 1], 1), -2), [[1, -2], [3, -2]]) + assert.deepStrictEqual(subset(d, index(new Range(0, 2), 1), -2), [[1, -2], [3, -2]]) + assert.deepStrictEqual(subset(g, index([0, 1], 1), -2), math.matrix([[1, -2], [3, -2]])) + assert.deepStrictEqual(subset(g, index(new Range(0, 2), 1), -2), math.matrix([[1, -2], [3, -2]])) + }) + + it('should throw an error if setting the subset of an array with an invalid array of booleans', function () { + assert.throws(function () { subset(d, index([true], 0), 123) }, DimensionError) + assert.throws(function () { subset(d, index(0, [true, false, true]), 123) }, DimensionError) + assert.throws(function () { subset(g, index([true], 0), 123) }, DimensionError) + assert.throws(function () { subset(g, index(0, [true, false, true]), 123) }, DimensionError) + assert.throws(function () { subset(d, index([true], 0), 123, 1) }, DimensionError) + assert.throws(function () { subset(d, index(0, [true, false, true]), 123, 1) }, DimensionError) + assert.throws(function () { subset(g, index([true], 0), 123, 1) }, DimensionError) + assert.throws(function () { subset(g, index(0, [true, false, true]), 123, 1) }, DimensionError) + }) + + it('should throw an error if setting the subset of an array with an invalid index', function () { assert.throws(function () { subset(d, index(1), 123) }, RangeError) assert.throws(function () { subset(d, index(1.3, 0), 123) }, TypeError) }) @@ -130,6 +217,15 @@ describe('subset', function () { const res = subset(obj, index('foo'), 'bar') assert.deepStrictEqual(res, { foo: 'bar' }) assert.deepStrictEqual(obj, {}) // should leave the original object untouched + const res2 = subset(obj, index(''), 'bar') + assert.deepStrictEqual(res2, {}) // should leave the original object untouched + }) + + it('should throw an error when attempting to index an object with something other than a string', function () { + const obj = { foo: 'bar' } + assert.throws(function () { subset(obj, index(1)) }, /TypeError/) + assert.throws(function () { subset(obj, index([1]), 1) }, /TypeError/) + assert.throws(function () { subset(obj, index([true]), 1, 1) }, /TypeError/) }) it('should throw an error if setting the subset of a string with an invalid replacement', function () { @@ -149,6 +245,7 @@ describe('subset', function () { it('should throw an error if in case of an invalid index type', function () { assert.throws(function () { subset('hello', 2) }, /TypeError: Unexpected type of argument/) assert.throws(function () { subset('hello', 2, 'A') }, /TypeError: Unexpected type of argument/) + assert.throws(function () { subset('hello', 2, 'A', 'B') }, /TypeError: Unexpected type of argument/) }) }) diff --git a/test/unit-tests/type/matrix/DenseMatrix.test.js b/test/unit-tests/type/matrix/DenseMatrix.test.js index b7193ae6f6..679ba465e0 100644 --- a/test/unit-tests/type/matrix/DenseMatrix.test.js +++ b/test/unit-tests/type/matrix/DenseMatrix.test.js @@ -613,9 +613,6 @@ describe('DenseMatrix', function () { m.subset(index(0, new Range(0, 2)), [1, 1]) // 2 assert.deepStrictEqual(m, new DenseMatrix([[1, 1], [0, 0]])) - m.subset(index(new Range(0, 2), 0), [2, 2]) // 2 - assert.deepStrictEqual(m, new DenseMatrix([[2, 1], [2, 0]])) - m = new DenseMatrix([[[0], [0], [0]]]) // 1x3x1 m.subset(index(0, new Range(0, 3), 0), [1, 2, 3]) // 3 assert.deepStrictEqual(m, new DenseMatrix([[[1], [2], [3]]])) @@ -696,7 +693,7 @@ describe('DenseMatrix', function () { it('should throw an error in case of dimension mismatch', function () { const m = new DenseMatrix([[1, 2, 3], [4, 5, 6]]) assert.throws(function () { m.subset(index(new Range(0, 2)), [100, 100]) }, /Dimension mismatch/) - assert.throws(function () { m.subset(index(new Range(0, 2), new Range(0, 2)), [100, 100]) }, /Dimension mismatch/) + assert.throws(function () { m.subset(index(new Range(0, 2), new Range(0, 2)), [100, 100, 100]) }, /Dimension mismatch/) }) }) diff --git a/test/unit-tests/utils/array.test.js b/test/unit-tests/utils/array.test.js index 9549e70a19..219ca89d30 100644 --- a/test/unit-tests/utils/array.test.js +++ b/test/unit-tests/utils/array.test.js @@ -1,4 +1,5 @@ import assert from 'assert' +import math from '../../../src/defaultInstance.js' import { arraySize, flatten, @@ -9,7 +10,14 @@ import { squeeze, unsqueeze, validate, - validateIndex + validateIndex, + isEmptyIndex, + broadcastSizes, + broadcastTo, + concat, + checkBroadcastingRules, + stretch, + broadcastArrays } from '../../../src/utils/array.js' describe('util.array', function () { @@ -51,6 +59,10 @@ describe('util.array', function () { }) describe('resize', function () { + it('should resize a scalar', function () { + const a = 0 + assert.deepStrictEqual(resize(a, [3]), [0, 0, 0]) + }) it('should resize a 1 dimensional array', function () { let a = [] @@ -582,4 +594,108 @@ describe('util.array', function () { assert.deepStrictEqual(generalize([{ value: 1, identifier: 0 }, { value: 1, identifier: 1 }, { value: 2, identifier: 0 }]), [1, 1, 2]) }) }) + + describe('broadcastSizes', function () { + it('should calculate the broadcasted sizes', function () { + assert.deepStrictEqual(broadcastSizes([1, 2], [2, 2]), [2, 2]) + assert.deepStrictEqual(broadcastSizes([3, 2], [1, 2], [3, 1]), [3, 2]) + }) + it('should throw an error when the broadcasting rules are not followed', function () { + assert.throws(function () { broadcastSizes([2, 2], [3, 2]) }, /Error: shape missmatch: missmatch is found in arg with shape.*/) + }) + }) + + describe('broadcastTo', function () { + it('should leave an array as such when broadcasting to the same size', function () { + const a = [10, 20] + const b = [[10, 20]] + assert.deepStrictEqual(broadcastTo(a, [2]), a) + assert.deepStrictEqual(broadcastTo(b, [1, 2]), b) + }) + it('should broadcast an array to a certain size', function () { + assert.deepStrictEqual(broadcastTo([10, 20], [2, 2]), [[10, 20], [10, 20]]) + assert.deepStrictEqual(broadcastTo([[10, 20]], [3, 2]), [[10, 20], [10, 20], [10, 20]]) + assert.deepStrictEqual(broadcastTo([1, 2, 3], [1, 2, 3]), [[[1, 2, 3], [1, 2, 3]]]) + }) + it('should throw an error when not possible to broadcast to', function () { + assert.throws(function () { broadcastTo([10, 20], [1]) }) + }) + }) + + describe('concat', function () { + it('should concat arrays', function () { + assert.deepStrictEqual(concat([[1, 2]], [[1, 2]], 0), [[1, 2], [1, 2]]) + assert.deepStrictEqual(concat([[1, 2]], [[1, 2]], 1), [[1, 2, 1, 2]]) + }) + it('should return arrays as such if only one is supplied', function () { + assert.deepStrictEqual(concat([[1, 2], [3, 4]], 0), [[1, 2], [3, 4]]) + assert.deepStrictEqual(concat([1, 2], 0), [1, 2]) + }) + it('should throw an error when the wrong number of arguments is supplied', function () { + assert.throws(function () { concat([[1, 2], [3, 4]]) }) + assert.throws(function () { concat(1) }) + assert.throws(function () { concat() }) + }) + }) + + describe('stretch', function () { + it('should stretch arrays in the specified direction', function () { + assert.deepStrictEqual(stretch([[1, 2]], 3, 0), [[1, 2], [1, 2], [1, 2]]) + assert.deepStrictEqual(stretch([[1, 2]], 3, 1), [[1, 2, 1, 2, 1, 2]]) + }) + }) + + describe('checkBroadcastingRules', function () { + it('should not throw an error if the broadcasting rules are ok', function () { + assert.doesNotThrow(function () { checkBroadcastingRules([1, 2], [1, 2]) }) + assert.doesNotThrow(function () { checkBroadcastingRules([1, 2], [2, 2]) }) + assert.doesNotThrow(function () { checkBroadcastingRules([2, 1], [2, 2]) }) + }) + it('should throw an error if the broadcasting rules are not ok', function () { + assert.throws(function () { checkBroadcastingRules([2, 2], [3, 2]) }) + assert.throws(function () { checkBroadcastingRules([2, 2], [2, 3]) }) + assert.throws(function () { checkBroadcastingRules([2, 2], [1, 2]) }) + }) + }) + + describe('broadcastArrays', function () { + it('should broadcast many arrays', function () { + assert.deepStrictEqual(broadcastArrays([1, 2], [3, 4]), [[1, 2], [3, 4]]) + assert.deepStrictEqual(broadcastArrays([1, 2], [[3], [4]]), [[[1, 2], [1, 2]], [[3, 3], [4, 4]]]) + assert.deepStrictEqual(broadcastArrays([1, 2], [[3], [4]], [5, 6]), [[[1, 2], [1, 2]], [[3, 3], [4, 4]], [[5, 6], [5, 6]]]) + }) + it('should broadcast leave arrays as such when only one is supplied', function () { + assert.deepStrictEqual(broadcastArrays([1, 2]), [1, 2], [3, 4]) + assert.deepStrictEqual(broadcastArrays([[3], [4]]), [[3], [4]]) + assert.deepStrictEqual(broadcastArrays([[5, 6]]), [[5, 6]]) + }) + it('should throw an arryor when the broadcasting rules don\'t apply', function () { + assert.throws(function () { broadcastArrays([1, 2], [1, 2, 3]) }) + assert.throws(function () { broadcastArrays([1, 2], [1, 2, 3], [4, 5]) }) + assert.throws(function () { broadcastArrays([[1, 2], [1, 2]], [[1, 2, 3]]) }) + }) + it('should throw an arryor when not enough arguments are supplied', function () { + assert.throws(function () { broadcastArrays() }) + }) + }) + describe('isEmptyIndex', function () { + it('should detect an empty index in arrays', function () { + assert.deepStrictEqual(isEmptyIndex(math.index([])), true) + assert.deepStrictEqual(isEmptyIndex(math.index(1)), false) + assert.deepStrictEqual(isEmptyIndex(math.index([], 1)), true) + assert.deepStrictEqual(isEmptyIndex(math.index(0, 1)), false) + }) + it('should detect an empty index in ranges', function () { + assert.deepStrictEqual(isEmptyIndex(math.index(new math.Range(0, 0))), true) + assert.deepStrictEqual(isEmptyIndex(math.index(new math.Range(0, 1))), false) + assert.deepStrictEqual(isEmptyIndex(math.index(new math.Range(0, 0), 1)), true) + assert.deepStrictEqual(isEmptyIndex(math.index(0, new math.Range(0, 1))), false) + }) + it('should detect an empty index in text', function () { + assert.deepStrictEqual(isEmptyIndex(math.index('')), true) + assert.deepStrictEqual(isEmptyIndex(math.index('someText')), false) + assert.deepStrictEqual(isEmptyIndex(math.index('', 1)), true) + assert.deepStrictEqual(isEmptyIndex(math.index(0, 'someText')), false) + }) + }) })