Skip to content

Commit 2c81deb

Browse files
committed
Add Matrices section with basic Matrix operations (multiplication, transposition, etc.)
1 parent e220450 commit 2c81deb

File tree

5 files changed

+852
-21
lines changed

5 files changed

+852
-21
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ a set of rules that precisely define a sequence of operations.
7777
* `B` [Radian & Degree](src/algorithms/math/radian) - radians to degree and backwards conversion
7878
* `B` [Fast Powering](src/algorithms/math/fast-powering)
7979
* `B` [Horner's method](src/algorithms/math/horner-method) - polynomial evaluation
80+
* `B` [Matrices](src/algorithms/math/matrix) - matrices and basic matrix operations (multiplication, transposition, etc.)
8081
* `A` [Integer Partition](src/algorithms/math/integer-partition)
8182
* `A` [Square Root](src/algorithms/math/square-root) - Newton's method
8283
* `A` [Liu Hui π Algorithm](src/algorithms/math/liu-hui) - approximate π calculations based on N-gons

src/algorithms/cryptography/hill-cipher/hillCipher.js

+24-21
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as mtrx from '../../math/matrix/Matrix';
2+
13
// The code of an 'A' character (equals to 65).
24
const alphabetCodeShift = 'A'.codePointAt(0);
35
const englishAlphabetSize = 26;
@@ -15,33 +17,36 @@ const generateKeyMatrix = (keyString) => {
1517
'Invalid key string length. The square root of the key string must be an integer',
1618
);
1719
}
18-
const keyMatrix = [];
1920
let keyStringIndex = 0;
20-
for (let i = 0; i < matrixSize; i += 1) {
21-
const keyMatrixRow = [];
22-
for (let j = 0; j < matrixSize; j += 1) {
21+
return mtrx.generate(
22+
[matrixSize, matrixSize],
23+
// Callback to get a value of each matrix cell.
24+
// The order the matrix is being filled in is from left to right, from top to bottom.
25+
() => {
2326
// A → 0, B → 1, ..., a → 32, b → 33, ...
2427
const charCodeShifted = (keyString.codePointAt(keyStringIndex)) % alphabetCodeShift;
25-
keyMatrixRow.push(charCodeShifted);
2628
keyStringIndex += 1;
27-
}
28-
keyMatrix.push(keyMatrixRow);
29-
}
30-
return keyMatrix;
29+
return charCodeShifted;
30+
},
31+
);
3132
};
3233

3334
/**
3435
* Generates a message vector from a given message.
3536
*
3637
* @param {string} message - the message to encrypt.
37-
* @return {number[]} messageVector
38+
* @return {number[][]} messageVector
3839
*/
3940
const generateMessageVector = (message) => {
40-
const messageVector = [];
41-
for (let i = 0; i < message.length; i += 1) {
42-
messageVector.push(message.codePointAt(i) % alphabetCodeShift);
43-
}
44-
return messageVector;
41+
return mtrx.generate(
42+
[message.length, 1],
43+
// Callback to get a value of each matrix cell.
44+
// The order the matrix is being filled in is from left to right, from top to bottom.
45+
(cellIndices) => {
46+
const rowIndex = cellIndices[0];
47+
return message.codePointAt(rowIndex) % alphabetCodeShift;
48+
},
49+
);
4550
};
4651

4752
/**
@@ -59,19 +64,17 @@ export function hillCipherEncrypt(message, keyString) {
5964
}
6065

6166
const keyMatrix = generateKeyMatrix(keyString);
67+
const messageVector = generateMessageVector(message);
6268

6369
// keyString.length must equal to square of message.length
6470
if (keyMatrix.length !== message.length) {
6571
throw new Error('Invalid key string length. The key length must be a square of message length');
6672
}
6773

68-
const messageVector = generateMessageVector(message);
74+
const cipherVector = mtrx.dot(keyMatrix, messageVector);
6975
let cipherString = '';
70-
for (let row = 0; row < keyMatrix.length; row += 1) {
71-
let item = 0;
72-
for (let column = 0; column < keyMatrix.length; column += 1) {
73-
item += keyMatrix[row][column] * messageVector[column];
74-
}
76+
for (let row = 0; row < cipherVector.length; row += 1) {
77+
const item = cipherVector[row];
7578
cipherString += String.fromCharCode((item % englishAlphabetSize) + alphabetCodeShift);
7679
}
7780

src/algorithms/math/matrix/Matrix.js

+309
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
/**
2+
* @typedef {number} Cell
3+
* @typedef {Cell[][]|Cell[][][]} Matrix
4+
* @typedef {number[]} Shape
5+
* @typedef {number[]} CellIndices
6+
*/
7+
8+
/**
9+
* Gets the matrix's shape.
10+
*
11+
* @param {Matrix} m
12+
* @returns {Shape}
13+
*/
14+
export const shape = (m) => {
15+
const shapes = [];
16+
let dimension = m;
17+
while (dimension && Array.isArray(dimension)) {
18+
shapes.push(dimension.length);
19+
dimension = (dimension.length && [...dimension][0]) || null;
20+
}
21+
return shapes;
22+
};
23+
24+
/**
25+
* Checks if matrix has a correct type.
26+
*
27+
* @param {Matrix} m
28+
* @throws {Error}
29+
*/
30+
const validateType = (m) => {
31+
if (
32+
!m
33+
|| !Array.isArray(m)
34+
|| !Array.isArray(m[0])
35+
) {
36+
throw new Error('Invalid matrix format');
37+
}
38+
};
39+
40+
/**
41+
* Checks if matrix is two dimensional.
42+
*
43+
* @param {Matrix} m
44+
* @throws {Error}
45+
*/
46+
const validate2D = (m) => {
47+
validateType(m);
48+
const aShape = shape(m);
49+
if (aShape.length !== 2) {
50+
throw new Error('Matrix is not of 2D shape');
51+
}
52+
};
53+
54+
/**
55+
* Validates that matrices are of the same shape.
56+
*
57+
* @param {Matrix} a
58+
* @param {Matrix} b
59+
* @trows {Error}
60+
*/
61+
const validateSameShape = (a, b) => {
62+
validateType(a);
63+
validateType(b);
64+
65+
const aShape = shape(a);
66+
const bShape = shape(b);
67+
68+
if (aShape.length !== bShape.length) {
69+
throw new Error('Matrices have different dimensions');
70+
}
71+
72+
while (aShape.length && bShape.length) {
73+
if (aShape.pop() !== bShape.pop()) {
74+
throw new Error('Matrices have different shapes');
75+
}
76+
}
77+
};
78+
79+
/**
80+
* Generates the matrix of specific shape with specific values.
81+
*
82+
* @param {Shape} mShape - the shape of the matrix to generate
83+
* @param {function({CellIndex}): Cell} fill - cell values of a generated matrix.
84+
* @returns {Matrix}
85+
*/
86+
export const generate = (mShape, fill) => {
87+
/**
88+
* Generates the matrix recursively.
89+
*
90+
* @param {Shape} recShape - the shape of the matrix to generate
91+
* @param {CellIndices} recIndices
92+
* @returns {Matrix}
93+
*/
94+
const generateRecursively = (recShape, recIndices) => {
95+
if (recShape.length === 1) {
96+
return Array(recShape[0])
97+
.fill(null)
98+
.map((cellValue, cellIndex) => fill([...recIndices, cellIndex]));
99+
}
100+
const m = [];
101+
for (let i = 0; i < recShape[0]; i += 1) {
102+
m.push(generateRecursively(recShape.slice(1), [...recIndices, i]));
103+
}
104+
return m;
105+
};
106+
107+
return generateRecursively(mShape, []);
108+
};
109+
110+
/**
111+
* Generates the matrix of zeros of specified shape.
112+
*
113+
* @param {Shape} mShape - shape of the matrix
114+
* @returns {Matrix}
115+
*/
116+
export const zeros = (mShape) => {
117+
return generate(mShape, () => 0);
118+
};
119+
120+
/**
121+
* @param {Matrix} a
122+
* @param {Matrix} b
123+
* @return Matrix
124+
* @throws {Error}
125+
*/
126+
export const dot = (a, b) => {
127+
// Validate inputs.
128+
validate2D(a);
129+
validate2D(b);
130+
131+
// Check dimensions.
132+
const aShape = shape(a);
133+
const bShape = shape(b);
134+
if (aShape[1] !== bShape[0]) {
135+
throw new Error('Matrices have incompatible shape for multiplication');
136+
}
137+
138+
// Perform matrix multiplication.
139+
const outputShape = [aShape[0], bShape[1]];
140+
const c = zeros(outputShape);
141+
142+
for (let bCol = 0; bCol < b[0].length; bCol += 1) {
143+
for (let aRow = 0; aRow < a.length; aRow += 1) {
144+
let cellSum = 0;
145+
for (let aCol = 0; aCol < a[aRow].length; aCol += 1) {
146+
cellSum += a[aRow][aCol] * b[aCol][bCol];
147+
}
148+
c[aRow][bCol] = cellSum;
149+
}
150+
}
151+
152+
return c;
153+
};
154+
155+
/**
156+
* Transposes the matrix.
157+
*
158+
* @param {Matrix} m
159+
* @returns Matrix
160+
* @throws {Error}
161+
*/
162+
export const t = (m) => {
163+
validate2D(m);
164+
const mShape = shape(m);
165+
const transposed = zeros([mShape[1], mShape[0]]);
166+
for (let row = 0; row < m.length; row += 1) {
167+
for (let col = 0; col < m[0].length; col += 1) {
168+
transposed[col][row] = m[row][col];
169+
}
170+
}
171+
return transposed;
172+
};
173+
174+
/**
175+
* Traverses the matrix.
176+
*
177+
* @param {Matrix} m
178+
* @param {function(indices: CellIndices, c: Cell)} visit
179+
*/
180+
const walk = (m, visit) => {
181+
/**
182+
* Traverses the matrix recursively.
183+
*
184+
* @param {Matrix} recM
185+
* @param {CellIndices} cellIndices
186+
* @return {Matrix}
187+
*/
188+
const recWalk = (recM, cellIndices) => {
189+
const recMShape = shape(recM);
190+
191+
if (recMShape.length === 1) {
192+
for (let i = 0; i < recM.length; i += 1) {
193+
visit([...cellIndices, i], recM[i]);
194+
}
195+
}
196+
for (let i = 0; i < recM.length; i += 1) {
197+
recWalk(recM[i], [...cellIndices, i]);
198+
}
199+
};
200+
201+
recWalk(m, []);
202+
};
203+
204+
/**
205+
* Gets the matrix cell value at specific index.
206+
*
207+
* @param {Matrix} m - Matrix that contains the cell that needs to be updated
208+
* @param {CellIndices} cellIndices - Array of cell indices
209+
* @return {Cell}
210+
*/
211+
const getCellAtIndex = (m, cellIndices) => {
212+
// We start from the row at specific index.
213+
let cell = m[cellIndices[0]];
214+
// Going deeper into the next dimensions but not to the last one to preserve
215+
// the pointer to the last dimension array.
216+
for (let dimIdx = 1; dimIdx < cellIndices.length - 1; dimIdx += 1) {
217+
cell = cell[cellIndices[dimIdx]];
218+
}
219+
// At this moment the cell variable points to the array at the last needed dimension.
220+
return cell[cellIndices[cellIndices.length - 1]];
221+
};
222+
223+
/**
224+
* Update the matrix cell at specific index.
225+
*
226+
* @param {Matrix} m - Matrix that contains the cell that needs to be updated
227+
* @param {CellIndices} cellIndices - Array of cell indices
228+
* @param {Cell} cellValue - New cell value
229+
*/
230+
const updateCellAtIndex = (m, cellIndices, cellValue) => {
231+
// We start from the row at specific index.
232+
let cell = m[cellIndices[0]];
233+
// Going deeper into the next dimensions but not to the last one to preserve
234+
// the pointer to the last dimension array.
235+
for (let dimIdx = 1; dimIdx < cellIndices.length - 1; dimIdx += 1) {
236+
cell = cell[cellIndices[dimIdx]];
237+
}
238+
// At this moment the cell variable points to the array at the last needed dimension.
239+
cell[cellIndices[cellIndices.length - 1]] = cellValue;
240+
};
241+
242+
/**
243+
* Adds two matrices element-wise.
244+
*
245+
* @param {Matrix} a
246+
* @param {Matrix} b
247+
* @return {Matrix}
248+
*/
249+
export const add = (a, b) => {
250+
validateSameShape(a, b);
251+
const result = zeros(shape(a));
252+
253+
walk(a, (cellIndices, cellValue) => {
254+
updateCellAtIndex(result, cellIndices, cellValue);
255+
});
256+
257+
walk(b, (cellIndices, cellValue) => {
258+
const currentCellValue = getCellAtIndex(result, cellIndices);
259+
updateCellAtIndex(result, cellIndices, currentCellValue + cellValue);
260+
});
261+
262+
return result;
263+
};
264+
265+
/**
266+
* Multiplies two matrices element-wise.
267+
*
268+
* @param {Matrix} a
269+
* @param {Matrix} b
270+
* @return {Matrix}
271+
*/
272+
export const mul = (a, b) => {
273+
validateSameShape(a, b);
274+
const result = zeros(shape(a));
275+
276+
walk(a, (cellIndices, cellValue) => {
277+
updateCellAtIndex(result, cellIndices, cellValue);
278+
});
279+
280+
walk(b, (cellIndices, cellValue) => {
281+
const currentCellValue = getCellAtIndex(result, cellIndices);
282+
updateCellAtIndex(result, cellIndices, currentCellValue * cellValue);
283+
});
284+
285+
return result;
286+
};
287+
288+
/**
289+
* Subtract two matrices element-wise.
290+
*
291+
* @param {Matrix} a
292+
* @param {Matrix} b
293+
* @return {Matrix}
294+
*/
295+
export const sub = (a, b) => {
296+
validateSameShape(a, b);
297+
const result = zeros(shape(a));
298+
299+
walk(a, (cellIndices, cellValue) => {
300+
updateCellAtIndex(result, cellIndices, cellValue);
301+
});
302+
303+
walk(b, (cellIndices, cellValue) => {
304+
const currentCellValue = getCellAtIndex(result, cellIndices);
305+
updateCellAtIndex(result, cellIndices, currentCellValue - cellValue);
306+
});
307+
308+
return result;
309+
};

0 commit comments

Comments
 (0)