mathjs/lib/utils/array.js
patgrasso 014e0e3ec0 Introduce reshape() to utils.array
`reshape()` takes an n-d array and a list of sizes for each dimension,
and fits the data into the specified shape. If the product of the sizes
of the new dimensions does not match that of the old, a DimensionError
is thrown.
2016-10-15 20:36:23 -04:00

416 lines
10 KiB
JavaScript

'use strict';
var number = require('./number');
var string = require('./string');
var object = require('./object');
var types = require('./types');
var DimensionError = require('../error/DimensionError');
var IndexError = require('../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
*/
exports.size = function (x) {
var 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) {
var i;
var len = array.length;
if (len != size[dim]) {
throw new DimensionError(len, size[dim]);
}
if (dim < size.length - 1) {
// recursively validate each child array
var dimNext = dim + 1;
for (i = 0; i < len; i++) {
var 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
*/
exports.validate = function(array, size) {
var 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);
}
};
// a constant used to specify an undefined defaultValue
exports.UNINITIALIZED = {};
/**
* 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. To leave new entries undefined,
* specify array.UNINITIALIZED as defaultValue
* @return {Array} array The resized array
*/
exports.resize = function(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
var _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) {
var i;
var elem;
var oldLen = array.length;
var newLen = size[dim];
var minLen = Math.min(oldLen, newLen);
// apply new length
array.length = newLen;
if (dim < size.length - 1) {
// non-last dimension
var 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];
}
}
if(defaultValue !== exports.UNINITIALIZED) {
// 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
*/
exports.reshape = function(array, sizes) {
var flatArray = exports.flatten(array);
var newArray;
var product = function (arr) {
return arr.reduce(function (prev, curr) {
return prev * curr;
});
};
try {
newArray = _reshape(flatArray, sizes);
} catch (e) {
if (e instanceof DimensionError) {
e.actual = product(sizes);
e.expected = 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) {
var accumulator = [];
var 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) {
var 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
var 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) {
var i, ii;
if (dim < dims) {
var 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
*/
exports.unsqueeze = function(array, dims, outer, size) {
var s = size || exports.size(array);
// unsqueeze outer dimensions
if (outer) {
for (var 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) {
var i, ii;
if (Array.isArray(array)) {
var next = dim + 1;
for (i = 0, ii = array.length; i < ii; i++) {
array[i] = _unsqueeze(array[i], dims, next);
}
}
else {
for (var 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)
*/
exports.flatten = function(array) {
if (!Array.isArray(array)) {
//if not an array, return as is
return array;
}
var flat = [];
array.forEach(function callback(value) {
if (Array.isArray(value)) {
value.forEach(callback); //traverse through sub-arrays recursively
}
else {
flat.push(value);
}
});
return flat;
};
/**
* Test whether an object is an array
* @param {*} value
* @return {boolean} isArray
*/
exports.isArray = Array.isArray;