mathjs
Version:
Math.js is an extensive math library for JavaScript and Node.js. It features a flexible expression parser and offers an integrated solution to work with numbers, big numbers, complex numbers, units, and matrices.
511 lines (452 loc) • 13.9 kB
JavaScript
var util = require('../util/index'),
DimensionError = require('../error/DimensionError'),
Index = require('./Index'),
number = util.number,
string = util.string,
array = util.array,
object = util.object,
isArray = Array.isArray,
validateIndex = array.validateIndex;
/**
* @constructor Matrix
*
* A Matrix is a wrapper around an Array. A matrix can hold a multi dimensional
* array. A matrix can be constructed as:
* var matrix = new Matrix(data)
*
* Matrix contains the functions to resize, get and set values, get the size,
* clone the matrix and to convert the matrix to a vector, array, or scalar.
* Furthermore, one can iterate over the matrix using map and forEach.
* The internal Array of the Matrix can be accessed using the function valueOf.
*
* Example usage:
* var matrix = new Matrix([[1, 2], [3, 4]);
* matix.size(); // [2, 2]
* matrix.resize([3, 2], 5);
* matrix.valueOf(); // [[1, 2], [3, 4], [5, 5]]
* matrix.subset([1,2]) // 3 (indexes are zero-based)
*
* @param {Array | Matrix} [data] A multi dimensional array
*/
function Matrix(data) {
if (!(this instanceof Matrix)) {
throw new SyntaxError('Constructor must be called with the new operator');
}
if (data instanceof Matrix) {
// clone data from a Matrix
this._data = data.clone()._data;
}
else if (isArray(data)) {
// use array
// replace nested Matrices with Arrays
this._data = preprocess(data);
}
else if (data != null) {
// unsupported type
throw new TypeError('Unsupported type of data (' + util.types.type(data) + ')');
}
else {
// nothing provided
this._data = [];
}
// verify the size of the array
this._size = array.size(this._data);
}
/**
* Test whether an object is a Matrix
* @param {*} object
* @return {Boolean} isMatrix
*/
Matrix.isMatrix = function isMatrix(object) {
return (object instanceof Matrix);
};
/**
* Get a subset of the matrix, or replace a subset of the matrix.
*
* Usage:
* var subset = matrix.subset(index) // retrieve subset
* var value = matrix.subset(index, replacement) // replace subset
*
* @param {Index} index
* @param {Array | Matrix | *} [replacement]
* @param {*} [defaultValue] Default value, filled in on new entries when
* the matrix is resized. If not provided,
* new matrix elements will be left undefined.
*/
Matrix.prototype.subset = function subset(index, replacement, defaultValue) {
switch (arguments.length) {
case 1:
return _get(this, index);
// intentional fall through
case 2:
case 3:
return _set(this, index, replacement, defaultValue);
default:
throw new SyntaxError('Wrong number of arguments');
}
};
/**
* Get a single element from the matrix.
* @param {Number[]} index Zero-based index
* @return {*} value
*/
Matrix.prototype.get = function get(index) {
if (!isArray(index)) {
throw new TypeError('Array expected');
}
if (index.length != this._size.length) {
throw new DimensionError(index.length, this._size.length);
}
var data = this._data;
for (var i = 0, ii = index.length; i < ii; i++) {
var index_i = index[i];
validateIndex(index_i, data.length);
data = data[index_i];
}
return object.clone(data);
};
/**
* Replace a single element in the matrix.
* @param {Number[]} index Zero-based index
* @param {*} value
* @param {*} [defaultValue] Default value, filled in on new entries when
* the matrix is resized. If not provided,
* new matrix elements will be left undefined.
* @return {Matrix} self
*/
Matrix.prototype.set = function set (index, value, defaultValue) {
var i, ii;
// validate input type and dimensions
if (!isArray(index)) {
throw new Error('Array expected');
}
if (index.length < this._size.length) {
throw new DimensionError(index.length, this._size.length, '<');
}
// enlarge matrix when needed
var size = index.map(function (i) {
return i + 1;
});
_fit(this, size, defaultValue);
// traverse over the dimensions
var data = this._data;
for (i = 0, ii = index.length - 1; i < ii; i++) {
var index_i = index[i];
validateIndex(index_i, data.length);
data = data[index_i];
}
// set new value
index_i = index[index.length - 1];
validateIndex(index_i, data.length);
data[index_i] = value;
return this;
};
/**
* Get a submatrix of this matrix
* @param {Matrix} matrix
* @param {Index} index Zero-based index
* @private
*/
function _get (matrix, index) {
if (!(index instanceof Index)) {
throw new TypeError('Invalid index');
}
var isScalar = index.isScalar();
if (isScalar) {
// return a scalar
return matrix.get(index.min());
}
else {
// validate dimensions
var size = index.size();
if (size.length != matrix._size.length) {
throw new DimensionError(size.length, matrix._size.length);
}
// retrieve submatrix
var submatrix = new Matrix(_getSubmatrix(matrix._data, index, size.length, 0));
// TODO: more efficient when creating an empty matrix and setting _data and _size manually
// squeeze matrix output
while (isArray(submatrix._data) && submatrix._data.length == 1) {
submatrix._data = submatrix._data[0];
submatrix._size.shift();
}
return submatrix;
}
}
/**
* Recursively get a submatrix of a multi dimensional matrix.
* Index is not checked for correct number of dimensions.
* @param {Array} data
* @param {Index} index
* @param {number} dims Total number of dimensions
* @param {number} dim Current dimension
* @return {Array} submatrix
* @private
*/
function _getSubmatrix (data, index, dims, dim) {
var last = (dim == dims - 1);
var range = index.range(dim);
if (last) {
return range.map(function (i) {
validateIndex(i, data.length);
return data[i];
});
}
else {
return range.map(function (i) {
validateIndex(i, data.length);
var child = data[i];
return _getSubmatrix(child, index, dims, dim + 1);
});
}
}
/**
* Replace a submatrix in this matrix
* Indexes are zero-based.
* @param {Matrix} matrix
* @param {Index} index
* @param {Matrix | Array | *} submatrix
* @param {*} [defaultValue] Default value, filled in on new entries when
* the matrix is resized. If not provided,
* new matrix elements will be left undefined.
* @return {Matrix} matrix
* @private
*/
function _set (matrix, index, submatrix, defaultValue) {
if (!(index instanceof Index)) {
throw new TypeError('Invalid index');
}
// get index size and check whether the index contains a single value
var iSize = index.size(),
isScalar = index.isScalar();
// calculate the size of the submatrix, and convert it into an Array if needed
var sSize;
if (submatrix instanceof Matrix) {
sSize = submatrix.size();
submatrix = submatrix.valueOf();
}
else {
sSize = array.size(submatrix);
}
if (isScalar) {
// set a scalar
// check whether submatrix is a scalar
if (sSize.length != 0) {
throw new TypeError('Scalar expected');
}
matrix.set(index.min(), submatrix, defaultValue);
}
else {
// set a submatrix
// validate dimensions
if (iSize.length < matrix._size.length) {
throw new DimensionError(iSize.length, matrix._size.length, '<');
}
// unsqueeze the submatrix when needed
for (var i = 0, ii = iSize.length - sSize.length; i < ii; i++) {
submatrix = [submatrix];
sSize.unshift(1);
}
// check whether the size of the submatrix matches the index size
if (!object.deepEqual(iSize, sSize)) {
throw new DimensionError(iSize, sSize);
}
// enlarge matrix when needed
var size = index.max().map(function (i) {
return i + 1;
});
_fit(matrix, size, defaultValue);
// insert the sub matrix
var dims = iSize.length,
dim = 0;
_setSubmatrix (matrix._data, index, submatrix, dims, dim);
}
return matrix;
}
/**
* Replace a submatrix of a multi dimensional matrix.
* @param {Array} data
* @param {Index} index
* @param {Array} submatrix
* @param {number} dims Total number of dimensions
* @param {number} dim
* @private
*/
function _setSubmatrix (data, index, submatrix, dims, dim) {
var last = (dim == dims - 1),
range = index.range(dim);
if (last) {
range.forEach(function (dataIndex, subIndex) {
validateIndex(dataIndex);
data[dataIndex] = submatrix[subIndex];
});
}
else {
range.forEach(function (dataIndex, subIndex) {
validateIndex(dataIndex);
_setSubmatrix(data[dataIndex], index, submatrix[subIndex], dims, dim + 1);
});
}
}
/**
* Resize the matrix
* @param {Number[]} size
* @param {*} [defaultValue] Default value, filled in on new entries.
* If not provided, the matrix elements will
* be left undefined.
* @return {Matrix} self The matrix itself is returned
*/
Matrix.prototype.resize = function resize(size, defaultValue) {
this._size = object.clone(size);
this._data = array.resize(this._data, this._size, defaultValue);
// return the matrix itself
return this;
};
/**
* Enlarge the matrix when it is smaller than given size.
* If the matrix is larger or equal sized, nothing is done.
* @param {Matrix} matrix The matrix to be resized
* @param {Number[]} size
* @param {*} [defaultValue] Default value, filled in on new entries.
* If not provided, the matrix elements will
* be left undefined.
* @private
*/
function _fit(matrix, size, defaultValue) {
var newSize = object.clone(matrix._size),
changed = false;
// add dimensions when needed
while (newSize.length < size.length) {
newSize.unshift(0);
changed = true;
}
// enlarge size when needed
for (var i = 0, ii = size.length; i < ii; i++) {
if (size[i] > newSize[i]) {
newSize[i] = size[i];
changed = true;
}
}
if (changed) {
// resize only when size is changed
matrix.resize(newSize, defaultValue);
}
}
/**
* Create a clone of the matrix
* @return {Matrix} clone
*/
Matrix.prototype.clone = function clone() {
var matrix = new Matrix();
matrix._data = object.clone(this._data);
matrix._size = object.clone(this._size);
return matrix;
};
/**
* Retrieve the size of the matrix.
* @returns {Number[]} size
*/
Matrix.prototype.size = function size() {
return this._size;
};
/**
* Create a new matrix with the results of the callback function executed on
* each entry of the matrix.
* @param {function} callback The callback function is invoked with three
* parameters: the value of the element, the index
* of the element, and the Matrix being traversed.
* @return {Matrix} matrix
*/
Matrix.prototype.map = function map(callback) {
var me = this;
var matrix = new Matrix();
var index = [];
var recurse = function (value, dim) {
if (isArray(value)) {
return value.map(function (child, i) {
index[dim] = i;
return recurse(child, dim + 1);
});
}
else {
return callback(value, index, me);
}
};
matrix._data = recurse(this._data, 0);
matrix._size = object.clone(this._size);
return matrix;
};
/**
* Execute a callback function on each entry of the matrix.
* @param {function} callback The callback function is invoked with three
* parameters: the value of the element, the index
* of the element, and the Matrix being traversed.
*/
Matrix.prototype.forEach = function forEach(callback) {
var me = this;
var index = [];
var recurse = function (value, dim) {
if (isArray(value)) {
value.forEach(function (child, i) {
index[dim] = i;
recurse(child, dim + 1);
});
}
else {
callback(value, index, me);
}
};
recurse(this._data, 0);
};
/**
* Create an Array with a copy of the data of the Matrix
* @returns {Array} array
*/
Matrix.prototype.toArray = function toArray() {
return object.clone(this._data);
};
/**
* Get the primitive value of the Matrix: a multidimensional array
* @returns {Array} array
*/
Matrix.prototype.valueOf = function valueOf() {
return this._data;
};
/**
* Get a string representation of the matrix, with optional formatting options.
* @param {Object | Number | Function} [options] Formatting options. See
* lib/util/number:format for a
* description of the available
* options.
* @returns {String} str
*/
Matrix.prototype.format = function format(options) {
return string.format(this._data, options);
};
/**
* Get a string representation of the matrix
* @returns {String} str
*/
Matrix.prototype.toString = function toString() {
return string.format(this._data);
};
/**
* Preprocess data, which can be an Array or Matrix with nested Arrays and
* Matrices. Replaces all nested Matrices with Arrays
* @param {Array} data
* @return {Array} data
*/
function preprocess(data) {
for (var i = 0, ii = data.length; i < ii; i++) {
var elem = data[i];
if (isArray(elem)) {
data[i] = preprocess(elem);
}
else if (elem instanceof Matrix) {
data[i] = preprocess(elem._data);
}
}
return data;
}
// exports
module.exports = Matrix;