Repository: josdejong/mathjs (js)

Issue:
reshape()
I think it might be useful to have a `reshape()` function that accepts a matrix and a list of dimensions, and formats the matrix to fit the specified dimensions. `resize()` does this already, but it doesn't preserve all of the elements.

```javascript
var A = [1, 2, 3, 4, 5, 6, 7, 8, 9];
A.resize([3, 3]);
/* => [[1, 0, 0],
       [2, 0, 0],
       [3, 0, 0]] */
A.reshape([3, 3]);
/* => [[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]] */
```

Thoughts? I can start working on it and submit a PR if this sounds appealing.

Pull request:
Implementation of reshape()
Contains implementation of `reshape()` (see #725). While very similar to `resize()`, this will retain all elements and requires the product of the dimensions to be the same (2x2 -> 1x4, 3x2x4 -> 12x2).

`reshape()` has been added as a global function `math.reshape()`, as well as a method on DenseMatrix `new DenseMatrix([[1, 2], [3, 4]]).reshape([1, 4])` for convenience.

Every location in which `reshape()` has been added contains a corresponding set of tests.

**Note**: I haven't figured out an efficient method for reshaping a sparse matrix, so for now the `SparseMatrix.reshape()` method does not exist.

Closes #725 

Patch:
diff --git a/lib/function/matrix/index.js b/lib/function/matrix/index.js
index 3c5681db4..e08d885c8 100644
--- a/lib/function/matrix/index.js
+++ b/lib/function/matrix/index.js
@@ -14,6 +14,7 @@ module.exports = [
   require('./ones'),
   require('./partitionSelect'),
   require('./range'),
+  require('./reshape'),
   require('./resize'),
   require('./size'),
   require('./sort'),
diff --git a/lib/function/matrix/reshape.js b/lib/function/matrix/reshape.js
new file mode 100644
index 000000000..87d37503f
--- /dev/null
+++ b/lib/function/matrix/reshape.js
@@ -0,0 +1,70 @@
+'use strict';
+
+var DimensionError = require('../../error/DimensionError');
+
+var isInteger = require('../../utils/number').isInteger;
+var array = require('../../utils/array');
+
+function factory (type, config, load, typed) {
+  var matrix = load(require('../../type/matrix/function/matrix'));
+
+  /**
+   * Reshape a multi dimensional array to fit the specified dimensions
+   *
+   * Syntax:
+   *
+   *     math.reshape(x, sizes)
+   *
+   * Examples:
+   *
+   *     math.reshape([1, 2, 3, 4, 5, 6], [2, 3]);
+   *     // returns Array  [[1, 2, 3], [4, 5, 6]]
+   *
+   *     math.reshape([[1, 2], [3, 4]], [1, 4]);
+   *     // returns Array  [[1, 2, 3, 4]]
+   *
+   *     math.reshape([[1, 2], [3, 4]], [4]);
+   *     // returns Array [1, 2, 3, 4]
+   *
+   *     var x = math.matrix([1, 2, 3, 4, 5, 6, 7, 8]);
+   *     math.reshape(x, [2, 2, 2]);
+   *     // returns Matrix [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
+   *
+   * See also:
+   *
+   *     size, squeeze, resize
+   *
+   * @param {Array | Matrix | *} x  Matrix to be reshaped
+   * @param {number[]} sizes        One dimensional array with integral sizes for
+   *                                each dimension
+   *
+   * @return {* | Array | Matrix}   A reshaped clone of matrix `x`
+   *
+   * @throws {TypeError}            If `sizes` does not contain solely integers
+   * @throws {DimensionError}       If the product of the new dimension sizes does
+   *                                not equal that of the old ones
+   */
+  var reshape = typed('reshape', {
+
+    'Matrix, Array': function (x, sizes) {
+      return matrix(array.reshape(x.valueOf(), sizes));
+    },
+
+    'Array, Array': function (x, sizes) {
+      sizes.forEach(function (size) {
+        if (!isInteger(size)) {
+          throw new TypeError('Invalid size for dimension: ' + size);
+        }
+      });
+      return array.reshape(x, sizes);
+    }
+
+  });
+
+  reshape.toTex = undefined; // use default template
+
+  return reshape;
+}
+
+exports.name = 'reshape';
+exports.factory = factory;
diff --git a/lib/type/matrix/DenseMatrix.js b/lib/type/matrix/DenseMatrix.js
index b010025ac..a614f79a6 100644
--- a/lib/type/matrix/DenseMatrix.js
+++ b/lib/type/matrix/DenseMatrix.js
@@ -430,6 +430,28 @@ function factory (type, config, load, typed) {
     // return matrix
     return matrix;
   };
+
+  /**
+   * Reshape the matrix to the given size. Returns a copy of the matrix when
+   * `copy=true`, otherwise return the matrix itself (reshape in place).
+   *
+   * NOTE: This might be better suited to copy by default, instead of modifying
+   *       in place. For now, it operates in place to remain consistent with
+   *       resize().
+   *
+   * @memberof DenseMatrix
+   * @param {number[]} size           The new size the matrix should have.
+   * @param {boolean} [copy]          Return a reshaped copy of the matrix
+   *
+   * @return {Matrix}                 The reshaped matrix
+   */
+  DenseMatrix.prototype.reshape = function (size, copy) {
+    var m = copy ? this.clone() : this;
+
+    m._data = array.reshape(m._data, size);
+    m._size = size.slice(0);
+    return m;
+  };
   
   /**
    * Enlarge the matrix when it is smaller than given size.
@@ -855,4 +877,4 @@ function factory (type, config, load, typed) {
 exports.name = 'DenseMatrix';
 exports.path = 'type';
 exports.factory = factory;
-exports.lazy = false;  // no lazy loading, as we alter type.Matrix._storage
\ No newline at end of file
+exports.lazy = false;  // no lazy loading, as we alter type.Matrix._storage
diff --git a/lib/type/matrix/ImmutableDenseMatrix.js b/lib/type/matrix/ImmutableDenseMatrix.js
index 8d554c7ec..742a9bf04 100644
--- a/lib/type/matrix/ImmutableDenseMatrix.js
+++ b/lib/type/matrix/ImmutableDenseMatrix.js
@@ -128,6 +128,15 @@ function factory (type, config, load) {
     throw new Error('Cannot invoke resize on an Immutable Matrix instance');
   };
 
+  /**
+   * Disallows reshaping in favor of immutability.
+   *
+   * @throws {Error} Operation not allowed
+   */
+  ImmutableDenseMatrix.prototype.reshape = function () {
+    throw new Error('Cannot invoke reshape on an Immutable Matrix instance');
+  };
+
   /**
    * Create a clone of the matrix
    * @return {ImmutableDenseMatrix} clone
diff --git a/lib/type/matrix/Matrix.js b/lib/type/matrix/Matrix.js
index 5218c32b4..53bff8efe 100644
--- a/lib/type/matrix/Matrix.js
+++ b/lib/type/matrix/Matrix.js
@@ -159,6 +159,20 @@ function factory (type, config, load, typed) {
     throw new Error('Cannot invoke resize on a Matrix interface');
   };
 
+  /**
+   * Reshape the matrix to the given size. Returns a copy of the matrix when
+   * `copy=true`, otherwise return the matrix itself (reshape in place).
+   *
+   * @param {number[]} size           The new size the matrix should have.
+   * @param {boolean} [copy]          Return a reshaped copy of the matrix
+   *
+   * @return {Matrix}                 The reshaped matrix
+   */
+  Matrix.prototype.reshape = function (size, defaultValue) {
+    // must be implemented by each of the Matrix implementations
+    throw new Error('Cannot invoke reshape on a Matrix interface');
+  };
+
   /**
    * Create a clone of the matrix
    * @return {Matrix} clone
diff --git a/lib/utils/array.js b/lib/utils/array.js
index e0f96a26a..6879b66b2 100644
--- a/lib/utils/array.js
+++ b/lib/utils/array.js
@@ -201,6 +201,85 @@ 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.<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;
+    });
+  };
+
+  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) {
+  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


Test patch:
diff --git a/test/function/matrix/reshape.test.js b/test/function/matrix/reshape.test.js
new file mode 100644
index 000000000..a02e8f1cb
--- /dev/null
+++ b/test/function/matrix/reshape.test.js
@@ -0,0 +1,68 @@
+var assert = require('assert'),
+    error = require('../../../lib/error/index'),
+    math = require('../../../index'),
+    Matrix = math.type.Matrix;
+
+describe('reshape', function() {
+
+  it('should reshape an array', function() {
+    var array = [[0,1,2],[3,4,5]];
+    assert.deepEqual(math.reshape(array, [3, 2]), [[0,1], [2,3], [4,5]]);
+  });
+
+  it('should reshape an array with bignumbers', function() {
+    var zero = math.bignumber(0);
+    var one = math.bignumber(1);
+    var two = math.bignumber(2);
+    var three = math.bignumber(3);
+    var array = [zero, one, two, three];
+    assert.deepEqual(math.reshape(array, [two, two]),
+        [[zero,one], [two, three]]);
+  });
+
+  it('should reshape a matrix', function() {
+    var matrix = math.matrix([[0,1,2],[3,4,5]]);
+    assert.deepEqual(math.reshape(matrix, [3, 2]),
+        math.matrix([[0,1], [2,3], [4,5]]));
+    assert.deepEqual(math.reshape(matrix, math.matrix([3, 2])),
+        math.matrix([[0,1], [2,3], [4,5]]));
+  });
+
+  it('should reshape a flat single-element array into multiple dimensions', function() {
+    var array = [3];
+    assert.deepEqual(math.reshape(array, [1, 1, 1]), [[[3]]]);
+  });
+
+  it('should reshape a vector into a 2d matrix', function() {
+    var math2 = math.create({matrix: 'Array'});
+    assert.deepEqual(math2.reshape([1,2,3,4,5,6], [3,2]), [[1, 2], [3, 4], [5, 6]]);
+  });
+
+  it('should reshape 2d matrix into a vector', function() {
+    var math2 = math.create({matrix: 'Array'});
+    assert.deepEqual(math2.reshape([[1,2],[3,4],[5,6]], [6]), [1,2,3,4,5,6]);
+  });
+
+  it('should throw an error on invalid arguments', function() {
+    assert.throws(function () {math.reshape()}, /Too few arguments/);
+    assert.throws(function () {math.reshape([])}, /Too few arguments/);
+    assert.throws(function () {math.reshape([], 2)}, TypeError);
+    assert.throws(function () {math.reshape([], [], 4)}, /Too many arguments/);
+
+    assert.throws(function () {math.reshape([], ['no number'])}, /Invalid size/);
+    assert.throws(function () {math.reshape([], [2.3])}, /Invalid size/);
+
+    assert.throws(function () {math.reshape([1, 2], [])}, error.DimensionError);
+    assert.throws(function () {math.reshape([1, 2], [0])}, error.DimensionError);
+    assert.throws(function () {math.reshape([1, 2], [0,0])}, error.DimensionError);
+    assert.throws(function () {math.reshape([[1, 2]], [0])}, error.DimensionError);
+    assert.doesNotThrow(function () {math.reshape([[1, 2]], [2,1])});
+    assert.doesNotThrow(function () {math.reshape([[1, 2]], [2])});
+  });
+
+  it('should LaTeX reshape', function () {
+    var expression = math.parse('reshape([1,2],1)');
+    assert.equal(expression.toTex(), '\\mathrm{reshape}\\left(\\begin{bmatrix}1\\\\2\\\\\\end{bmatrix},1\\right)');
+  });
+});
+
diff --git a/test/type/matrix/DenseMatrix.test.js b/test/type/matrix/DenseMatrix.test.js
index ecd10641f..fce87a1e8 100644
--- a/test/type/matrix/DenseMatrix.test.js
+++ b/test/type/matrix/DenseMatrix.test.js
@@ -334,6 +334,44 @@ describe('DenseMatrix', function() {
         ]);
     });
   });
+
+  describe('reshape', function () {
+
+    it('should reshape the matrix properly', function () {
+      var m = new DenseMatrix([[1,2,3],[4,5,6]]);
+      m.reshape([3,2]);
+      assert.deepEqual(m.valueOf(), [[1,2], [3,4], [5,6]]);
+      m.reshape([6,1]);
+      assert.deepEqual(m.valueOf(), [[1],[2],[3],[4],[5],[6]]);
+    });
+
+    it('should return a copy only when specified', function () {
+      var m1 = new DenseMatrix([[1, 2], [3, 4]]);
+      var m2 = m1.reshape([4]);
+      var m3 = m2.reshape([1, 4], true);
+
+      assert.strictEqual(m2, m1);
+      assert.deepEqual(m2.valueOf(), [1, 2, 3, 4]);
+      assert.deepEqual(m2.valueOf(), m1.valueOf());
+
+      assert.notStrictEqual(m3, m2);
+      assert.deepEqual(m3.valueOf(), [[1, 2, 3, 4]]);
+      assert.notDeepEqual(m3.valueOf(), m2.valueOf());
+    });
+
+    it('should update the size of the reshaped matrix', function () {
+      var m1 = new DenseMatrix([[1, 2], [3, 4]]);
+      var m2 = m1.reshape([4], true);
+
+      assert.deepEqual(m1.size(), [2, 2]);
+
+      m1.reshape([1, 1, 4]);
+
+      assert.deepEqual(m1.size(), [1, 1, 4]);
+      assert.deepEqual(m2.size(), [4]);
+    });
+
+  });
   
   describe('get', function () {
 
@@ -1340,4 +1378,4 @@ describe('DenseMatrix', function() {
     }
     return array;
   }
-});  
\ No newline at end of file
+});  
diff --git a/test/type/matrix/ImmutableDenseMatrix.test.js b/test/type/matrix/ImmutableDenseMatrix.test.js
index 804003ed5..8a729a75b 100644
--- a/test/type/matrix/ImmutableDenseMatrix.test.js
+++ b/test/type/matrix/ImmutableDenseMatrix.test.js
@@ -202,8 +202,18 @@ describe('ImmutableDenseMatrix', function() {
 
     it('should throw an exception on resize', function() {
       var m = new ImmutableDenseMatrix([[1,2,3],[4,5,6]]);
-      assert.throws(function () { m.resize([2,4]); }, /Cannot invoke resize on an Immutable Matrix instance/);      
+      assert.throws(function () { m.resize([2,4]); }, /Cannot invoke resize on an Immutable Matrix instance/);
     });
+
+  });
+
+  describe('reshape', function() {
+
+    it('should throw an exception on reshape', function() {
+      var m = new ImmutableDenseMatrix([[1,2,3],[4,5,6]]);
+      assert.throws(function () { m.reshape([6,1]); }, /Cannot invoke reshape on an Immutable Matrix instance/);
+    });
+
   });
 
   describe('get', function () {
@@ -610,4 +620,4 @@ describe('ImmutableDenseMatrix', function() {
       assert.throws(function () { m.swapRows(1, 2); }, /Cannot invoke swapRows on an Immutable Matrix instance/);      
     });
   });
-});  
\ No newline at end of file
+});  
diff --git a/test/type/matrix/Matrix.test.js b/test/type/matrix/Matrix.test.js
index c09dbc2b5..0067091ed 100644
--- a/test/type/matrix/Matrix.test.js
+++ b/test/type/matrix/Matrix.test.js
@@ -66,6 +66,14 @@ describe('matrix', function() {
       assert.throws(function () { m.resize(); }, /Cannot invoke resize on a Matrix interface/);
     });
   });
+
+  describe('reshape', function() {
+
+    it('should throw exception', function() {
+      var m = new Matrix();
+      assert.throws(function () { m.reshape(); }, /Cannot invoke reshape on a Matrix interface/);
+    });
+  });
   
   describe('get', function() {
 
@@ -114,4 +122,4 @@ describe('matrix', function() {
       assert.throws(function () { m.clone(); }, /Cannot invoke clone on a Matrix interface/);
     });
   });
-});
\ No newline at end of file
+});
diff --git a/test/utils/array.test.js b/test/utils/array.test.js
index a7005a9d6..f2bc24ad1 100644
--- a/test/utils/array.test.js
+++ b/test/utils/array.test.js
@@ -1,6 +1,7 @@
 var assert = require('assert'),
     array = require('../../lib/utils/array'),
-    resize = array.resize;
+    resize = array.resize,
+    reshape = array.reshape,
     size = array.size;
 
 describe('util.array', function() {
@@ -226,6 +227,98 @@ describe('util.array', function() {
     });
   });
 
+  describe('reshape', function () {
+
+    it('should reshape a 1 dimensional array into a 2 dimensional array', function () {
+      var a = [1, 2, 3, 4, 5, 6, 7, 8];
+
+      assert.deepEqual(
+        reshape(a, [2, 4]),
+        [[1, 2, 3, 4],
+         [5, 6, 7, 8]]
+      );
+      assert.deepEqual(
+        reshape(a, [4, 2]),
+        [[1, 2],
+         [3, 4],
+         [5, 6],
+         [7, 8]]
+      );
+      assert.deepEqual(
+        reshape(a, [1, 8]),
+        [[1, 2, 3, 4, 5, 6, 7, 8]]
+      );
+      assert.deepEqual(
+        reshape(a, [1, 1, 8]),
+        [[[1, 2, 3, 4, 5, 6, 7, 8]]]
+      );
+    });
+
+    it('should reshape a 2 dimensional array into a 1 dimensional array', function () {
+      var a = [
+        [0, 1],
+        [2, 3]
+      ];
+
+      assert.deepEqual(
+        reshape(a, [4]),
+        [0, 1, 2, 3]
+      );
+    });
+
+    it('should reshape a 3 dimensional array', function () {
+      var a = [[[1, 2],
+                [3, 4]],
+
+               [[5, 6],
+                [7, 8]]];
+
+      assert.deepEqual(
+        reshape(a, [8]),
+        [1, 2, 3, 4, 5, 6, 7, 8]
+      );
+
+      assert.deepEqual(
+        reshape(a, [2, 4]),
+        [[1, 2, 3, 4],
+         [5, 6, 7, 8]]
+      );
+
+    });
+
+    it('should throw an error when reshaping to a dimension with length 0', function () {
+      assert.throws(function () {reshape([1, 2], [0, 2]);}, /DimensionError/);
+      assert.throws(function () {reshape([1, 2], [2, 0]);}, /DimensionError/);
+    });
+
+    it('should throw an error when reshaping a non-empty array to an empty array', function () {
+      assert.throws(function () {reshape([1], []);}, /DimensionError/);
+      assert.throws(function () {reshape([1, 2], []);}, /DimensionError/);
+    });
+
+    it('should throw an error when reshaping to a size that differs from the original', function () {
+      var a = [1, 2, 3, 4, 5, 6, 7, 8, 9];
+
+      assert.deepEqual(
+        reshape(a, [3, 3]),
+        [[1, 2, 3],
+         [4, 5, 6],
+         [7, 8, 9]]
+      );
+      assert.throws(function () {reshape(a, [3, 2]);}, /DimensionError/);
+      assert.throws(function () {reshape(a, [2, 3]);}, /DimensionError/);
+      assert.throws(function () {reshape(a, [3, 3, 3]);}, /DimensionError/);
+      assert.throws(function () {reshape(a, [3, 4]);}, /DimensionError/);
+      assert.throws(function () {reshape(a, [4, 3]);}, /DimensionError/);
+    });
+
+    it('should throw an error in case of wrong type of arguments', function () {
+      assert.throws(function () {reshape([], 2);}, /Array expected/);
+      assert.throws(function () {reshape(2);}, /Array expected/);
+    });
+
+  });
+
   describe('squeeze', function () {
 
     it('should squeeze a scalar', function () {
@@ -518,4 +611,4 @@ function arr() {
     }
   }
   return array;
-}
\ No newline at end of file
+}