Skip to content

Commit f41dc82

Browse files
authored
feat: Require array values. Remove unsafe type casts. (#65)
2 parents 1a04675 + d151ade commit f41dc82

File tree

3 files changed

+91
-103
lines changed

3 files changed

+91
-103
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Run `npm install --save @smockle/matrix` to add `matrix` to your project.
1515
import Matrix from '@smockle/matrix'
1616

1717
// 1x1 Matrix
18-
const m11 = Matrix(3)
18+
const m11 = Matrix([3])
1919

2020
// 1x3 Matrix
2121
const m13 = Matrix([1, 2, 3])

src/__tests__/index.test.ts

+6-24
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@ describe("Matrix", () => {
1717
expect(Matrix.bind(Matrix, [[1, 2], [3], [4, 5]])).toThrowError(error);
1818
});
1919

20-
test("does not throw number", () => {
21-
expect(Matrix.bind(Matrix, 1)).not.toThrowError(error);
22-
});
23-
2420
test("does not throw number array", () => {
2521
expect(Matrix.bind(Matrix, [1])).not.toThrowError(error);
2622
});
@@ -276,8 +272,8 @@ describe("Matrix.multiply", () => {
276272
).toThrowError(new TypeError("Matrices are not multipliable"));
277273
});
278274

279-
test("multiply number", () => {
280-
expect(Matrix(3).multiply(Matrix(6))).toStrictEqual(Matrix(18));
275+
test("multiply A (1x1) and B (1x1)", () => {
276+
expect(Matrix([3]).multiply(Matrix([6]))).toStrictEqual(Matrix([18]));
281277
});
282278

283279
test("multiply A (1x2) and B (2x1)", () => {
@@ -340,8 +336,10 @@ describe("Matrix#multiply", () => {
340336
).toThrowError(new TypeError("Matrices are not multipliable"));
341337
});
342338

343-
test("multiply number", () => {
344-
expect(Matrix.multiply(Matrix(3), Matrix(6))).toStrictEqual(Matrix(18));
339+
test("multiply A (1x1) and B (1x1)", () => {
340+
expect(Matrix.multiply(Matrix([3]), Matrix([6]))).toStrictEqual(
341+
Matrix([18])
342+
);
345343
});
346344

347345
test("multiply A (1x2) and B (2x1)", () => {
@@ -401,10 +399,6 @@ describe("Matrix#valueOf", () => {
401399
});
402400

403401
describe("Matrix#countRows", () => {
404-
test("countRows number", () => {
405-
expect(Matrix(1).countRows()).toBe(0);
406-
});
407-
408402
test("countRows number array", () => {
409403
expect(Matrix([1]).countRows()).toBe(1);
410404
});
@@ -421,10 +415,6 @@ describe("Matrix#countRows", () => {
421415
});
422416

423417
describe("Matrix#countColumns", () => {
424-
test("countColumns number", () => {
425-
expect(Matrix(1).countColumns()).toBe(0);
426-
});
427-
428418
test("countColumns number array", () => {
429419
expect(Matrix([1]).countColumns()).toBe(1);
430420
});
@@ -445,10 +435,6 @@ describe("Matrix#countColumns", () => {
445435
});
446436

447437
describe("Matrix#transpose", () => {
448-
test("transpose number", () => {
449-
expect(Matrix(1).transpose()).toStrictEqual(Matrix(1));
450-
});
451-
452438
test("transpose number array", () => {
453439
expect(Matrix([1, 2]).transpose()).toStrictEqual(Matrix([[1], [2]]));
454440
});
@@ -506,10 +492,6 @@ describe("Matrix#map", () => {
506492
});
507493

508494
describe("Matrix#inspect", () => {
509-
test("inspect number", () => {
510-
expect(inspect(Matrix(3))).toBe("3");
511-
});
512-
513495
test("inspect number array", () => {
514496
expect(inspect(Matrix([1, 2, 3]))).toBe("[ 1 2 3 ]");
515497
});

src/index.ts

+84-78
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { fill, padStart, unzip } from "lodash-es";
22
import { inv } from "mathjs";
33

4-
type Matrix = {
5-
__value: number | (number | number[])[];
4+
interface IMatrix {
65
countRows: () => number;
76
countColumns: () => number;
87
addable: (y: Matrix) => boolean;
@@ -12,30 +11,43 @@ type Matrix = {
1211
transpose: () => Matrix;
1312
invert: () => Matrix;
1413
map: (x: any) => Matrix;
15-
valueOf: () => number | (number | number[])[];
16-
};
14+
}
15+
16+
interface Matrix1D extends IMatrix {
17+
__value: number[];
18+
valueOf: () => number[];
19+
}
20+
21+
interface Matrix2D extends IMatrix {
22+
__value: number[][];
23+
valueOf: () => number[][];
24+
}
25+
26+
type Matrix = Matrix1D | Matrix2D;
27+
28+
function isMatrix1D(matrix: Matrix): matrix is Matrix1D {
29+
return matrix.countRows() === 1;
30+
}
1731

1832
/**
1933
* Creates a Matrix
2034
* @constructor
2135
* @alias module:matrix
22-
* @param {number|(number | number[])[]} x - Values to store in matrix
36+
* @param {number[] | number[][]} x - Values to store in matrix
2337
* @throws {TypeError} Argument x must be a number or number array
2438
* @return {Matrix} Single or multi dimensional matrix
2539
*/
26-
function Matrix(x: number | (number | number[])[]): Matrix {
40+
function Matrix(x: number[] | number[][]): Matrix {
2741
// extra nesting
28-
if (Array.isArray(x) && Array.isArray(x[0]) && x.length === 1) {
42+
if (Array.isArray(x[0]) && x.length === 1) {
2943
throw new TypeError("Matrix must be a number or array of numbers");
3044
}
3145

3246
// uneven rows
47+
const firstRowLength = Array.isArray(x[0]) ? x[0].length : 0;
3348
if (
34-
Array.isArray(x) &&
35-
Array.isArray(x[0]) &&
36-
x.some(
37-
(row) => Array.isArray(row) && row.length !== (x[0] as number[]).length
38-
)
49+
firstRowLength > 0 &&
50+
x.some((row) => Array.isArray(row) && row.length !== firstRowLength)
3951
) {
4052
throw new TypeError("Matrix must be a number or array of numbers");
4153
}
@@ -70,10 +82,9 @@ Matrix.addable = function (x: Matrix, y: Matrix): boolean {
7082
Matrix.add = function (x: Matrix, y: Matrix): Matrix {
7183
if (!Matrix.addable(x, y)) throw new TypeError("Matrices are not addable");
7284
return x.map((row: number[], i: number): number[] =>
73-
row.map(
74-
(column: number, j: number): number =>
75-
column + (y.__value as number[][])[i][j]
76-
)
85+
row.map((column: number, j: number): number => {
86+
return column + (Array.isArray(y.__value[i]) ? y.__value[i][j] : 0);
87+
})
7788
);
7889
};
7990

@@ -82,7 +93,7 @@ Matrix.add = function (x: Matrix, y: Matrix): Matrix {
8293
* @alias module:matrix.multipliable
8394
* @param {Matrix} x - Matrix to check
8495
* @param {Matrix} y - Matrix to check
85-
* @return {boolean} Whether two matrices can be summed (using matrix multiplication)
96+
* @return {boolean} Whether two matrices can be multiplied (using matrix multiplication)
8697
*/
8798
Matrix.multipliable = function (x: Matrix, y: Matrix): boolean {
8899
return x.countColumns() === y.countRows();
@@ -95,16 +106,18 @@ Matrix.multipliable = function (x: Matrix, y: Matrix): boolean {
95106
* @param {number} i - Column in matrix y to multiply
96107
* @return {number} Inner product of matrices
97108
*/
98-
function innerproduct(x: Matrix, y: Matrix, i: number): number {
99-
const _x: number[] = x.__value as number[];
100-
const _y: number[] =
101-
Array.isArray(unzip<number>(y.__value as number[][])) &&
102-
unzip<number>(y.__value as number[][]).length === 0
103-
? unzip([y.__value as number[]])[i]
104-
: unzip(y.__value as number[][])[i];
105-
return ([] as number[])
106-
.concat(_x)
107-
.reduce((z: number, _z: number, j: number): number => z + _z * _y[j], 0);
109+
function innerproduct(x: Matrix1D, y: Matrix, i: number): number {
110+
const _x = x.__value;
111+
let _y;
112+
if (isMatrix1D(y)) {
113+
_y = unzip([y.__value])[i];
114+
} else {
115+
_y = unzip(y.__value)[i];
116+
}
117+
return [..._x].reduce(
118+
(z: number, _z: number, j: number): number => z + _z * _y[j],
119+
0
120+
);
108121
}
109122

110123
/**
@@ -119,23 +132,21 @@ Matrix.multiply = function (x: Matrix, y: Matrix): Matrix {
119132
throw new TypeError("Matrices are not multipliable");
120133
}
121134

122-
if (x.countColumns() === 0 && y.countRows() === 0) {
123-
return Matrix((x.__value as number) * (y.__value as number));
124-
}
125-
126135
/* New matrix with the dot product */
127-
const z: Matrix = Matrix(
128-
fill(
129-
Array(x.countRows()),
130-
x.countRows() !== 1 ? fill(Array(y.countColumns()), 0) : 0
131-
)
132-
);
133-
return z.map((_z: number | number[], i: number): number | number[] => {
134-
if (typeof _z === "number") return innerproduct(x, y, i);
135-
return _z.map((_, j) =>
136-
innerproduct(Matrix((x.__value as number[])[i]), y, j)
136+
if (isMatrix1D(x)) {
137+
return Matrix([0]).map((_z: number, i: number): number =>
138+
innerproduct(x, y, i)
137139
);
138-
});
140+
} else {
141+
return Matrix(
142+
fill(Array(x.countRows()), fill(Array(y.countColumns()), 0))
143+
).map((_z: number[], i: number) => {
144+
const _x = Matrix(x.__value[i]);
145+
if (isMatrix1D(_x)) {
146+
return _z.map((_, j): number => innerproduct(_x, y, j));
147+
}
148+
});
149+
}
139150
};
140151

141152
/**
@@ -154,7 +165,6 @@ Matrix.invert = function (x: Matrix): Matrix {
154165
* @return {number} Number of rows
155166
*/
156167
Matrix.prototype.countRows = function (this: Matrix): number {
157-
if (typeof this.__value === "number") return 0;
158168
if (typeof this.__value[0] === "number") return 1;
159169
return this.__value.length;
160170
};
@@ -165,7 +175,6 @@ Matrix.prototype.countRows = function (this: Matrix): number {
165175
* @return {number} Number of columns
166176
*/
167177
Matrix.prototype.countColumns = function (this: Matrix): number {
168-
if (typeof this.__value === "number") return 0;
169178
if (typeof this.__value[0] === "number") return this.__value.length;
170179
return this.__value[0].length;
171180
};
@@ -216,13 +225,10 @@ Matrix.prototype.multiply = function (this: Matrix, y: Matrix): Matrix {
216225
* @return {Matrix} New matrix with the transpose
217226
*/
218227
Matrix.prototype.transpose = function (this: Matrix): Matrix {
219-
switch (this.countRows()) {
220-
case 0:
221-
return Matrix(this.__value as number);
222-
case 1:
223-
return Matrix(unzip([this.__value as number[]]));
224-
default:
225-
return Matrix(unzip(this.__value as number[][]));
228+
if (isMatrix1D(this)) {
229+
return Matrix(unzip([this.__value]));
230+
} else {
231+
return Matrix(unzip(this.__value));
226232
}
227233
};
228234

@@ -240,19 +246,23 @@ Matrix.prototype.invert = function (this: Matrix): Matrix {
240246
* @alias module:matrix#map
241247
* @return {Matrix} Matrix inverse
242248
*/
243-
Matrix.prototype.map = function (this: Matrix, x: any): Matrix {
244-
if (typeof this.__value === "number") return Matrix(x(this.__value));
245-
return Matrix(this.__value.map(x));
249+
Matrix.prototype.map = function (
250+
this: Matrix,
251+
x: <T extends number | number[]>(value: T, index: number, array: T[]) => T
252+
): Matrix {
253+
if (isMatrix1D(this)) {
254+
return Matrix(this.__value.map(x<number>));
255+
} else {
256+
return Matrix(this.__value.map(x<number[]>));
257+
}
246258
};
247259

248260
/**
249261
* Returns the number or number array value
250262
* @alias module:matrix#valueOf
251-
* @return {number|number[]} Number of number array value
263+
* @return {number[]|number[][]} Number of number array value
252264
*/
253-
Matrix.prototype.valueOf = function (
254-
this: Matrix
255-
): number | (number | number[])[] {
265+
Matrix.prototype.valueOf = function (this: Matrix): number[] | number[][] {
256266
return this.__value;
257267
};
258268

@@ -264,26 +274,22 @@ Matrix.prototype.valueOf = function (
264274
Matrix.prototype[Symbol.for("nodejs.util.inspect.custom")] = function (
265275
this: Matrix
266276
): string {
267-
switch (this.countRows()) {
268-
case 0:
269-
return `${this.__value}`;
270-
case 1:
271-
return `[ ${(this.__value as number[]).join(" ")} ]`;
272-
default:
273-
/* Output array filled with zeroes */
274-
const padding: number[] = unzip(this.__value as number[][]).map(
275-
(column: number[]) =>
276-
column.reduce((length, x) => Math.max(`${x}`.length, length), 0)
277-
);
278-
return (this.__value as number[][])
279-
.reduce(
280-
(output, row) =>
281-
`${output}[ ${row
282-
.map((x, i) => padStart(`${x}`, padding[i]))
283-
.join(" ")} ]`,
284-
""
285-
)
286-
.replace(/]\[/g, "]\n[");
277+
if (isMatrix1D(this)) {
278+
return `[ ${this.__value.join(" ")} ]`;
279+
} else {
280+
/* Output array filled with zeroes */
281+
const padding: number[] = unzip(this.__value).map((column: number[]) =>
282+
column.reduce((length, x) => Math.max(`${x}`.length, length), 0)
283+
);
284+
return this.__value
285+
.reduce(
286+
(output, row) =>
287+
`${output}[ ${row
288+
.map((x, i) => padStart(`${x}`, padding[i]))
289+
.join(" ")} ]`,
290+
""
291+
)
292+
.replace(/]\[/g, "]\n[");
287293
}
288294
};
289295

0 commit comments

Comments
 (0)