import { isInteger } from './number' import { isNumber } from './is' import { format } from './string' import { DimensionError } from '../error/DimensionError' import { IndexError } from '../error/IndexError' /** * Calculate the size of a multi dimensional array. * This function checks the size of the first entry, it does not validate * whether all dimensions match. (use function `validate` for that) * @param {Array} x * @Return {Number[]} size */ export function arraySize (x) { const s = [] while (Array.isArray(x)) { s.push(x.length) x = x[0] } return s } /** * Recursively validate whether each element in a multi dimensional array * has a size corresponding to the provided size array. * @param {Array} array Array to be validated * @param {number[]} size Array with the size of each dimension * @param {number} dim Current dimension * @throws DimensionError * @private */ function _validate (array, size, dim) { let i const len = array.length if (len !== size[dim]) { throw new DimensionError(len, size[dim]) } if (dim < size.length - 1) { // recursively validate each child array const dimNext = dim + 1 for (i = 0; i < len; i++) { const child = array[i] if (!Array.isArray(child)) { throw new DimensionError(size.length - 1, size.length, '<') } _validate(array[i], size, dimNext) } } else { // last dimension. none of the childs may be an array for (i = 0; i < len; i++) { if (Array.isArray(array[i])) { throw new DimensionError(size.length + 1, size.length, '>') } } } } /** * Validate whether each element in a multi dimensional array has * a size corresponding to the provided size array. * @param {Array} array Array to be validated * @param {number[]} size Array with the size of each dimension * @throws DimensionError */ export function validate (array, size) { const isScalar = (size.length === 0) if (isScalar) { // scalar if (Array.isArray(array)) { throw new DimensionError(array.length, 0) } } else { // array _validate(array, size, 0) } } /** * Test whether index is an integer number with index >= 0 and index < length * when length is provided * @param {number} index Zero-based index * @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 < 0 || (typeof length === 'number' && index >= length)) { throw new IndexError(index, length) } } /** * 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 {*} [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 * set. * @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)) { throw new TypeError('Array expected') } if (size.length === 0) { throw new Error('Resizing to scalar is not supported') } // check whether size contains positive integers size.forEach(function (value) { if (!isNumber(value) || !isInteger(value) || value < 0) { throw new TypeError('Invalid size, must contain positive integers ' + '(size: ' + format(size) + ')') } }) // recursively resize the array const _defaultValue = (defaultValue !== undefined) ? defaultValue : 0 _resize(array, size, 0, _defaultValue) return array } /** * Recursively resize a multi dimensional array * @param {Array} array Array to be resized * @param {number[]} size Array with the size of each dimension * @param {number} dim Current dimension * @param {*} [defaultValue] Value to be filled in in new entries, * undefined by default. * @private */ function _resize (array, size, dim, defaultValue) { let i let elem const oldLen = array.length const newLen = size[dim] const minLen = Math.min(oldLen, newLen) // apply new length array.length = newLen if (dim < size.length - 1) { // non-last dimension const dimNext = dim + 1 // resize existing child arrays for (i = 0; i < minLen; i++) { // resize child array elem = array[i] if (!Array.isArray(elem)) { elem = [elem] // add a dimension array[i] = elem } _resize(elem, size, dimNext, defaultValue) } // create new child arrays for (i = minLen; i < newLen; i++) { // get child array elem = [] array[i] = elem // resize new child array _resize(elem, size, dimNext, defaultValue) } } else { // last dimension // remove dimensions of existing values for (i = 0; i < minLen; i++) { while (Array.isArray(array[i])) { array[i] = array[i][0] } } // fill new elements with the default value for (i = minLen; i < newLen; i++) { array[i] = 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 * @returns {Array} Array whose data has been formatted to fit the * specified dimensions * * @throws {DimensionError} If the product of the new dimension sizes does * not equal that of the old ones */ export function reshape (array, sizes) { const flatArray = flatten(array) let newArray function product (arr) { return arr.reduce((prev, curr) => prev * curr) } if (!Array.isArray(array) || !Array.isArray(sizes)) { throw new TypeError('Array expected') } if (sizes.length === 0) { throw new DimensionError(0, product(arraySize(array)), '!=') } let totalSize = 1 for (let sizeIndex = 0; sizeIndex < sizes.length; sizeIndex++) { totalSize *= sizes[sizeIndex] } if (flatArray.length !== totalSize) { throw new DimensionError( product(sizes), product(arraySize(array)), '!=' ) } try { newArray = _reshape(flatArray, sizes) } catch (e) { if (e instanceof DimensionError) { throw new DimensionError( product(sizes), product(arraySize(array)), '!=' ) } throw e } return newArray } /** * 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 * @returns {Array} Array whose data has been formatted to fit the * specified dimensions */ function _reshape (array, sizes) { // testing if there are enough elements for the requested shape let tmpArray = array let tmpArray2 // for each dimensions starting by the last one and ignoring the first one for (let sizeIndex = sizes.length - 1; sizeIndex > 0; sizeIndex--) { const size = sizes[sizeIndex] tmpArray2 = [] // aggregate the elements of the current tmpArray in elements of the requested size const length = tmpArray.length / size for (let i = 0; i < length; i++) { tmpArray2.push(tmpArray.slice(i * size, (i + 1) * size)) } // set it as the new tmpArray for the next loop turn or for return tmpArray = tmpArray2 } return tmpArray } /** * Squeeze a multi dimensional array * @param {Array} array * @param {Array} [size] * @returns {Array} returns the array itself */ export function squeeze (array, size) { const s = size || arraySize(array) // squeeze outer dimensions while (Array.isArray(array) && array.length === 1) { array = array[0] s.shift() } // find the first dimension to be squeezed let dims = s.length while (s[dims - 1] === 1) { dims-- } // squeeze inner dimensions if (dims < s.length) { array = _squeeze(array, dims, 0) s.length = dims } return array } /** * Recursively squeeze a multi dimensional array * @param {Array} array * @param {number} dims Required number of dimensions * @param {number} dim Current dimension * @returns {Array | *} Returns the squeezed array * @private */ function _squeeze (array, dims, dim) { let i, ii if (dim < dims) { const next = dim + 1 for (i = 0, ii = array.length; i < ii; i++) { array[i] = _squeeze(array[i], dims, next) } } else { while (Array.isArray(array)) { array = array[0] } } return array } /** * Unsqueeze a multi dimensional array: add dimensions when missing * * Paramter `size` will be mutated to match the new, unqueezed matrix size. * * @param {Array} array * @param {number} dims Desired number of dimensions of the array * @param {number} [outer] Number of outer dimensions to be added * @param {Array} [size] Current size of array. * @returns {Array} returns the array itself * @private */ export function unsqueeze (array, dims, outer, size) { const s = size || arraySize(array) // unsqueeze outer dimensions if (outer) { for (let i = 0; i < outer; i++) { array = [array] s.unshift(1) } } // unsqueeze inner dimensions array = _unsqueeze(array, dims, 0) while (s.length < dims) { s.push(1) } return array } /** * Recursively unsqueeze a multi dimensional array * @param {Array} array * @param {number} dims Required number of dimensions * @param {number} dim Current dimension * @returns {Array | *} Returns the squeezed array * @private */ function _unsqueeze (array, dims, dim) { let i, ii if (Array.isArray(array)) { const next = dim + 1 for (i = 0, ii = array.length; i < ii; i++) { array[i] = _unsqueeze(array[i], dims, next) } } else { for (let d = dim; d < dims; d++) { array = [array] } } return array } /** * Flatten a multi dimensional array, put all elements in a one dimensional * array * @param {Array} array A multi dimensional array * @return {Array} The flattened array (1 dimensional) */ export function flatten (array) { if (!Array.isArray(array)) { // if not an array, return as is return array } const flat = [] array.forEach(function callback (value) { if (Array.isArray(value)) { value.forEach(callback) // traverse through sub-arrays recursively } else { flat.push(value) } }) return flat } /** * A safe map * @param {Array} array * @param {function} callback */ export function map (array, callback) { return Array.prototype.map.call(array, callback) } /** * A safe forEach * @param {Array} array * @param {function} callback */ export function forEach (array, callback) { Array.prototype.forEach.call(array, callback) } /** * A safe filter * @param {Array} array * @param {function} callback */ export function filter (array, callback) { if (arraySize(array).length !== 1) { throw new Error('Only one dimensional matrices supported') } return Array.prototype.filter.call(array, callback) } /** * Filter values in a callback given a regular expression * @param {Array} array * @param {RegExp} regexp * @return {Array} Returns the filtered array * @private */ export function filterRegExp (array, regexp) { if (arraySize(array).length !== 1) { throw new Error('Only one dimensional matrices supported') } return Array.prototype.filter.call(array, (entry) => regexp.test(entry)) } /** * A safe join * @param {Array} array * @param {string} separator */ export function join (array, separator) { return Array.prototype.join.call(array, separator) } /** * Assign a numeric identifier to every element of a sorted array * @param {Array} a An array * @return {Array} An array of objects containing the original value and its identifier */ export function identify (a) { if (!Array.isArray(a)) { throw new TypeError('Array input expected') } if (a.length === 0) { return a } const b = [] let count = 0 b[0] = { value: a[0], identifier: 0 } for (let i = 1; i < a.length; i++) { if (a[i] === a[i - 1]) { count++ } else { count = 0 } b.push({ value: a[i], identifier: count }) } return b } /** * Remove the numeric identifier from the elements * @param {array} a An array * @return {array} An array of values without identifiers */ export function generalize (a) { if (!Array.isArray(a)) { throw new TypeError('Array input expected') } if (a.length === 0) { return a } const b = [] for (let i = 0; i < a.length; i++) { b.push(a[i].value) } return b } /** * Check the datatype of a given object * This is a low level implementation that should only be used by * parent Matrix classes such as SparseMatrix or DenseMatrix * This method does not validate Array Matrix shape * @param {Array} array * @param {function} typeOf Callback function to use to determine the type of a value * @return string */ export function getArrayDataType (array, typeOf) { let type // to hold type info let length = 0 // to hold length value to ensure it has consistent sizes for (let i = 0; i < array.length; i++) { const item = array[i] const isArray = Array.isArray(item) // Saving the target matrix row size if (i === 0 && isArray) { length = item.length } // If the current item is an array but the length does not equal the targetVectorSize if (isArray && item.length !== length) { return undefined } const itemType = isArray ? getArrayDataType(item, typeOf) // recurse into a nested array : typeOf(item) if (type === undefined) { type = itemType // first item } else if (type !== itemType) { return 'mixed' } else { // we're good, everything has the same type so far } } return type } /** * Return the last item from an array * @param array * @returns {*} */ export function last (array) { return array[array.length - 1] } /** * Get all but the last element of array. */ export function initial (array) { return array.slice(0, array.length - 1) } /** * Test whether an array or string contains an item * @param {Array | string} array * @param {*} item * @return {boolean} */ export function contains (array, item) { return array.indexOf(item) !== -1 }