Objective
Create a SquareMatrix
class in JavaScript to create and easily work with square matrices.
Description
In the exercises, we created a couple of functions to accomplish some common matrix operations such as addition, multiplication, rotation and transposition.
Now, you have to encapsulate all those into one single utility — the SquareMatrix
class.
Let's explore what bells and whistles do we need to give to this class.
The SquareMatrix
class defines two instance properties:
array
— holds the array used to represent the underlying matrix.n
— holds the length of the matrix (which is same in both dimensions, since we're dealing with square matrics).
Apart from these properties, the class defines a total of six instance methods, four of which follow from the previous exercises, as described below:
add(squareMatrixInstance)
— adds the calling matrix tosquareMatrixInstance
(another instance ofSquareMatrix
) and returns the sum. This method creates and returns a new matrix (instance ofSquareMatrix
). It doesn't mutate either the calling matrix or thesquareMatrixInstance
argument.multiply(squareMatrixInstance)
— multiplies the calling matrix tosquareMatrixInstance
and returns back the product. Akin tosum()
, this method creates and returns a new matrix, and doesn't mutate either the calling matrix or thesquareMatrixInstance
argument.copy()
— copies the calling matrix and returns back the copy.fill(value)
— replaces each element of the calling matrix withvalue
. In other words, the calling matrix is 'filled' up with the givenvalue
. The calling matrix is mutated. Nothing is returned by this method.rotateBy90()
— rotates the calling matrix clockwise by 90°. The calling matrix is mutated. Nothing is returned by this method.transpose()
— tranposes the calling matrix. The calling matrix is mutated. Nothing is returned by this method as well.
Note that the first three methods — add()
, multiply()
and copy()
— are non-mutating i.e. they don't modify the original calling matrix in any way. A new matrix is returned at the end.
Similarly, the last three methods — fill()
, rotateBy90()
and transpose()
— are mutating i.e. they modify the original calling matrix. Nothing is returned at the end.
When the constructor function SquareMatrix()
is called with a single integer argument n
, an n
x n
array is created with each element initialized to 0
, and this array stored in the array
property. In the meanwhile, the property n
is initialized as well.
Otherwise, it's assumed that the function is called with an array as argument, and henceforth, the array is used as-is to create the matrix i.e. the given array argument is set as the value of the array
property and n
is updated likewise.
Given all these details, you have to create this SquareMatrix
class in JavaScript using the idea of constructors and prototypes.
New file
Inside the directory you created for this course on JavaScript, create a new folder called Exercise-37-Square-Matrix-Class and put the .html solution files for this exercise within it.
Solution
The specification of SquareMatrix
class, as defined above, is quite clear to give us a clue about how to implement it.
Let's recap it. We need to have two instance properties: array
and n
, and six instance methods: add()
, multiply()
, copy()
, fill()
, rotateBy90()
, transpose()
.
First, let's define the constructor function SquareMatrix()
:
function SquareMatrix(matrixArray) {
// If matrixArray is a number,
// Create an n x n matrix
if (typeof matrixArray === 'number') {
var n = matrixArray;
matrixArray = [];
for (var i = 0; i < n; i++) {
matrixArray.push([]);
for (var j = 0; j < n; j++) {
matrixArray[i].push(0);
}
}
}
this.array = matrixArray;
this.n = matrixArray.length;
}
In here, we check the type of the argument passed in: if it's a number, we store that number inside a local variable n
and then create an n
x n
array, with all 0
s, held on in the variable matrixArray
.
In the end, we set the instance properties array
and n
to matrixArray
and the length of matrixArray
, respectively.
The constructor's done.
Next up, we need to define all of the six instance methods.
First, let's get done with the first three non-mutating methods, of which two (add()
and multiply()
) are already implemented in the previous exercises.
We'll start with add()
:
SquareMatrix.prototype.add = function(matrix) {
// Initializations
var matrixArray = this.array;
var secondMatrixArray = matrix.array;
var n = this.n;
// Create a new matrix to represent the sum
var sumMatrix = new SquareMatrix(n);
var sumMatrixArray = sumMatrix.array;
for (var i = 0; i < n; i++) {
for (var j = 0; j < n; j++) {
sumMatrixArray[i][j] = matrixArray[i][j] + secondMatrixArray[i][j];
}
}
return sumMatrix;
}
Note that from the previous implementation of add()
, in the JavaScript Arrays — Matrix Arithmetic Exercise, we've changed quite a lot of stuff here and there and even added some new stuff.
First of all, the local variable matrixArray
holds the array
of the calling matrix. Note that this is the same name (i.e matrixArray
) that we used in the constructor function SquareMatrix()
.
matrixArray
in SquareMatrix()
and then something like thisArray
(or simply array
) in add()
.Being consistent not only helps in understanding the code much better and much quicker, but also makes sure that we don't mistakenly introduce bugs into our program by accessing a variable directly when we had to access a property on that variable instead.
Moving on, then we define a variable secondMatrixArray
to hold the array
of the given matrix
argument. Finally, we save this.n
inside the variable n
.
Next up, we create a new n
x n
matrix to hold the sum, store that in sumMatrix
, and then save the value of its array
property in sumMatrixArray
for quick access later on inside the for
loop.
Notice how we append the name of every variable with the word 'Array' when that variable holds the array
of a given SquareMatrix
instance. This is an example of consistency.
Now, let's move on to multiply()
.
SquareMatrix.prototype.multiply = function(matrix) {
// Initializations
var matrixArray = this.array;
var secondMatrixArray = matrix.array;
var n = this.n;
// Create a new matrix to represent the product
var productMatrix = new SquareMatrix(n);
var productMatrixArray = productMatrix.array;
var sum;
for (var i = 0; i < n; i++) {
for (var j = 0; j < n; j++) {
sum = 0;
for (var k = 0; k < n; k++) {
sum += matrixArray[i][k] * secondMatrixArray[k][j];
}
productMatrixArray[i][j] = sum;
}
}
return productMatrix;
}
As you can see, the initializations here are the same as in add()
and the naming of productMatrix
and productMatrix
is also quite similar to the sumMatrix
and sumMatrixArray
.
Moving on to copy()
:
SquareMatrix.prototype.copy = function(matrix) {
// Initializations
var matrixArray = this.array;
var n = this.n;
// Create a new matrix to hold the copy
var copiedMatrix = new SquareMatrix(n);
var copiedMatrixArray = copiedMatrix.array;
for (var i = 0; i < n; i++) {
for (var j = 0; j < n; j++) {
copiedMatrixArray[i][j] = matrixArray[i][j];
}
}
return copiedMatrix;
}
copy()
is very easy to implement — just iterate over the calling matrix and copy each value on to the new matrix.
Hopefully, now you're getting the gist of the naming of variables and their behavior in these methods.
With this, we are done with the definition of our three non-mutating methods. It's time to address the remaining three, mutating, methods.
First, let's consider fill()
:
SquareMatrix.prototype.fill = function(value) {
// Initializations
var matrixArray = this.array;
var n = this.n;
for (var i = 0; i < n; i++) {
for (var j = 0; j < n; j++) {
matrixArray[i][j] = value;
}
}
// Return nothing
}
fill()
simply iterates over the entire matrixArray
and assigns the argument value
to each position, effectively making every element of the calling matrix equal to value
.
Now, over to rotateBy90()
:
SquareMatrix.prototype.rotateBy90 = function() {
// Initializations
var matrixArray = this.array;
var n = this.n;
// Copy the calling matrix using its copy() method
var copiedMatrixArray = this.copy().array;
for (var i = 0; i < n; i++) {
for (var j = 0; j < n; j++) {
matrixArray[i][j] = copiedMatrixArray[n - j - 1][i];
}
}
// Return nothing
}
Notice how we've removed the first set of nested for
loops (as we had previously in the final code snippet in JavaScript Arrays — Matrix Rotation Exercise) from the method's definition here and used the copy()
method instead.
This is an example of something called code reuse.
We define the matrix copying implementation once inside the copy()
method and then re-use from a given SquareMatrix
instance method very conveniently.
Finally, let's get done with transpose()
:
SquareMatrix.prototype.transpose = function() {
// Initializations
var matrixArray = this.array;
var n = this.n;
for (var i = 0; i < n - 1; i++) {
for (var j = i + 1; j < n; j++) {
temp = matrixArray[i][j];
matrixArray[i][j] = matrixArray[j][i];
matrixArray[j][i] = temp;
}
}
// Return nothing
}
Altogether, our code looks something as follows:
function SquareMatrix(matrixArray) {
/* ... */
}
// Non-mutating methods
SquareMatrix.prototype.add = function(matrix) { /* ... */ }
SquareMatrix.prototype.multiply = function(matrix) { /* ... */ }
SquareMatrix.prototype.copy = function() { /* ... */ }
// Mutating methods
SquareMatrix.prototype.fill = function(value) { /* ... */ }
SquareMatrix.prototype.rotateBy90 = function() { /* ... */ }
SquareMatrix.prototype.transpose = function() { /* ... */ }