mathjs/lib/utils/array.js
2018-06-06 14:31:33 +02:00

524 lines
13 KiB
JavaScript

'use strict'
import number from './number'
import string 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 size(x) {
let 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
*/
exports.validateIndex = function(index, length) {
if (!number.isNumber(index) || !number.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.<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
* 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 (!number.isNumber(value) || !number.isInteger(value) || value < 0) {
throw new TypeError('Invalid size, must contain positive integers ' +
'(size: ' + string.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.<number>} 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 = exports.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(exports.size(array)), '!=')
}
try {
newArray = _reshape(flatArray, sizes)
} catch (e) {
if (e instanceof DimensionError) {
throw new DimensionError(
product(sizes),
product(exports.size(array)),
'!='
)
}
throw e
}
if (flatArray.length > 0) {
throw new DimensionError(
product(sizes),
product(exports.size(array)),
'!='
)
}
return newArray
}
/**
* Recursively re-shape a multi dimensional array to fit the specified dimensions
* @param {Array} array Array to be reshaped
* @param {Array.<number>} 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
*/
function _reshape(array, sizes) {
let accumulator = []
let i
if (sizes.length === 0) {
if (array.length === 0) {
throw new DimensionError(null, null, '!=')
}
return array.shift()
}
for (i = 0; i < sizes[0]; i += 1) {
accumulator.push(_reshape(array, sizes.slice(1)))
}
return accumulator
}
/**
* Squeeze a multi dimensional array
* @param {Array} array
* @param {Array} [size]
* @returns {Array} returns the array itself
*/
exports.squeeze = function(array, size) {
let s = size || exports.size(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) {
let s = size || exports.size(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
}
let 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 (exports.size(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 (exports.size(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
}
let 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
*/
exports.generalize = function(a) {
if (!Array.isArray(a)) {
throw new TypeError('Array input expected')
}
if (a.length === 0) {
return a
}
let b = []
for (let i=0; i<a.length; i++) {
b.push(a[i].value)
}
return b
}
/**
* Test whether an object is an array
* @param {*} value
* @return {boolean} isArray
*/
exports.isArray = Array.isArray