Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Require array values. Remove unsafe type casts. #65

Merged
merged 2 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Run `npm install --save @smockle/matrix` to add `matrix` to your project.
import Matrix from '@smockle/matrix'

// 1x1 Matrix
const m11 = Matrix(3)
const m11 = Matrix([3])

// 1x3 Matrix
const m13 = Matrix([1, 2, 3])
Expand Down
30 changes: 6 additions & 24 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ describe("Matrix", () => {
expect(Matrix.bind(Matrix, [[1, 2], [3], [4, 5]])).toThrowError(error);
});

test("does not throw number", () => {
expect(Matrix.bind(Matrix, 1)).not.toThrowError(error);
});

test("does not throw number array", () => {
expect(Matrix.bind(Matrix, [1])).not.toThrowError(error);
});
Expand Down Expand Up @@ -276,8 +272,8 @@ describe("Matrix.multiply", () => {
).toThrowError(new TypeError("Matrices are not multipliable"));
});

test("multiply number", () => {
expect(Matrix(3).multiply(Matrix(6))).toStrictEqual(Matrix(18));
test("multiply A (1x1) and B (1x1)", () => {
expect(Matrix([3]).multiply(Matrix([6]))).toStrictEqual(Matrix([18]));
});

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

test("multiply number", () => {
expect(Matrix.multiply(Matrix(3), Matrix(6))).toStrictEqual(Matrix(18));
test("multiply A (1x1) and B (1x1)", () => {
expect(Matrix.multiply(Matrix([3]), Matrix([6]))).toStrictEqual(
Matrix([18])
);
});

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

describe("Matrix#countRows", () => {
test("countRows number", () => {
expect(Matrix(1).countRows()).toBe(0);
});

test("countRows number array", () => {
expect(Matrix([1]).countRows()).toBe(1);
});
Expand All @@ -421,10 +415,6 @@ describe("Matrix#countRows", () => {
});

describe("Matrix#countColumns", () => {
test("countColumns number", () => {
expect(Matrix(1).countColumns()).toBe(0);
});

test("countColumns number array", () => {
expect(Matrix([1]).countColumns()).toBe(1);
});
Expand All @@ -445,10 +435,6 @@ describe("Matrix#countColumns", () => {
});

describe("Matrix#transpose", () => {
test("transpose number", () => {
expect(Matrix(1).transpose()).toStrictEqual(Matrix(1));
});

test("transpose number array", () => {
expect(Matrix([1, 2]).transpose()).toStrictEqual(Matrix([[1], [2]]));
});
Expand Down Expand Up @@ -506,10 +492,6 @@ describe("Matrix#map", () => {
});

describe("Matrix#inspect", () => {
test("inspect number", () => {
expect(inspect(Matrix(3))).toBe("3");
});

test("inspect number array", () => {
expect(inspect(Matrix([1, 2, 3]))).toBe("[ 1 2 3 ]");
});
Expand Down
162 changes: 84 additions & 78 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { fill, padStart, unzip } from "lodash-es";
import { inv } from "mathjs";

type Matrix = {
__value: number | (number | number[])[];
interface IMatrix {
countRows: () => number;
countColumns: () => number;
addable: (y: Matrix) => boolean;
Expand All @@ -12,30 +11,43 @@ type Matrix = {
transpose: () => Matrix;
invert: () => Matrix;
map: (x: any) => Matrix;
valueOf: () => number | (number | number[])[];
};
}

interface Matrix1D extends IMatrix {
__value: number[];
valueOf: () => number[];
}

interface Matrix2D extends IMatrix {
__value: number[][];
valueOf: () => number[][];
}

type Matrix = Matrix1D | Matrix2D;

function isMatrix1D(matrix: Matrix): matrix is Matrix1D {
return matrix.countRows() === 1;
}

/**
* Creates a Matrix
* @constructor
* @alias module:matrix
* @param {number|(number | number[])[]} x - Values to store in matrix
* @param {number[] | number[][]} x - Values to store in matrix
* @throws {TypeError} Argument x must be a number or number array
* @return {Matrix} Single or multi dimensional matrix
*/
function Matrix(x: number | (number | number[])[]): Matrix {
function Matrix(x: number[] | number[][]): Matrix {
// extra nesting
if (Array.isArray(x) && Array.isArray(x[0]) && x.length === 1) {
if (Array.isArray(x[0]) && x.length === 1) {
throw new TypeError("Matrix must be a number or array of numbers");
}

// uneven rows
const firstRowLength = Array.isArray(x[0]) ? x[0].length : 0;
if (
Array.isArray(x) &&
Array.isArray(x[0]) &&
x.some(
(row) => Array.isArray(row) && row.length !== (x[0] as number[]).length
)
firstRowLength > 0 &&
x.some((row) => Array.isArray(row) && row.length !== firstRowLength)
) {
throw new TypeError("Matrix must be a number or array of numbers");
}
Expand Down Expand Up @@ -70,10 +82,9 @@ Matrix.addable = function (x: Matrix, y: Matrix): boolean {
Matrix.add = function (x: Matrix, y: Matrix): Matrix {
if (!Matrix.addable(x, y)) throw new TypeError("Matrices are not addable");
return x.map((row: number[], i: number): number[] =>
row.map(
(column: number, j: number): number =>
column + (y.__value as number[][])[i][j]
)
row.map((column: number, j: number): number => {
return column + (Array.isArray(y.__value[i]) ? y.__value[i][j] : 0);
})
);
};

Expand All @@ -82,7 +93,7 @@ Matrix.add = function (x: Matrix, y: Matrix): Matrix {
* @alias module:matrix.multipliable
* @param {Matrix} x - Matrix to check
* @param {Matrix} y - Matrix to check
* @return {boolean} Whether two matrices can be summed (using matrix multiplication)
* @return {boolean} Whether two matrices can be multiplied (using matrix multiplication)
*/
Matrix.multipliable = function (x: Matrix, y: Matrix): boolean {
return x.countColumns() === y.countRows();
Expand All @@ -95,16 +106,18 @@ Matrix.multipliable = function (x: Matrix, y: Matrix): boolean {
* @param {number} i - Column in matrix y to multiply
* @return {number} Inner product of matrices
*/
function innerproduct(x: Matrix, y: Matrix, i: number): number {
const _x: number[] = x.__value as number[];
const _y: number[] =
Array.isArray(unzip<number>(y.__value as number[][])) &&
unzip<number>(y.__value as number[][]).length === 0
? unzip([y.__value as number[]])[i]
: unzip(y.__value as number[][])[i];
return ([] as number[])
.concat(_x)
.reduce((z: number, _z: number, j: number): number => z + _z * _y[j], 0);
function innerproduct(x: Matrix1D, y: Matrix, i: number): number {
const _x = x.__value;
let _y;
if (isMatrix1D(y)) {
_y = unzip([y.__value])[i];
} else {
_y = unzip(y.__value)[i];
}
return [..._x].reduce(
(z: number, _z: number, j: number): number => z + _z * _y[j],
0
);
}

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

if (x.countColumns() === 0 && y.countRows() === 0) {
return Matrix((x.__value as number) * (y.__value as number));
}

/* New matrix with the dot product */
const z: Matrix = Matrix(
fill(
Array(x.countRows()),
x.countRows() !== 1 ? fill(Array(y.countColumns()), 0) : 0
)
);
return z.map((_z: number | number[], i: number): number | number[] => {
if (typeof _z === "number") return innerproduct(x, y, i);
return _z.map((_, j) =>
innerproduct(Matrix((x.__value as number[])[i]), y, j)
if (isMatrix1D(x)) {
return Matrix([0]).map((_z: number, i: number): number =>
innerproduct(x, y, i)
);
});
} else {
return Matrix(
fill(Array(x.countRows()), fill(Array(y.countColumns()), 0))
).map((_z: number[], i: number) => {
const _x = Matrix(x.__value[i]);
if (isMatrix1D(_x)) {
return _z.map((_, j): number => innerproduct(_x, y, j));
}
});
}
};

/**
Expand All @@ -154,7 +165,6 @@ Matrix.invert = function (x: Matrix): Matrix {
* @return {number} Number of rows
*/
Matrix.prototype.countRows = function (this: Matrix): number {
if (typeof this.__value === "number") return 0;
if (typeof this.__value[0] === "number") return 1;
return this.__value.length;
};
Expand All @@ -165,7 +175,6 @@ Matrix.prototype.countRows = function (this: Matrix): number {
* @return {number} Number of columns
*/
Matrix.prototype.countColumns = function (this: Matrix): number {
if (typeof this.__value === "number") return 0;
if (typeof this.__value[0] === "number") return this.__value.length;
return this.__value[0].length;
};
Expand Down Expand Up @@ -216,13 +225,10 @@ Matrix.prototype.multiply = function (this: Matrix, y: Matrix): Matrix {
* @return {Matrix} New matrix with the transpose
*/
Matrix.prototype.transpose = function (this: Matrix): Matrix {
switch (this.countRows()) {
case 0:
return Matrix(this.__value as number);
case 1:
return Matrix(unzip([this.__value as number[]]));
default:
return Matrix(unzip(this.__value as number[][]));
if (isMatrix1D(this)) {
return Matrix(unzip([this.__value]));
} else {
return Matrix(unzip(this.__value));
}
};

Expand All @@ -240,19 +246,23 @@ Matrix.prototype.invert = function (this: Matrix): Matrix {
* @alias module:matrix#map
* @return {Matrix} Matrix inverse
*/
Matrix.prototype.map = function (this: Matrix, x: any): Matrix {
if (typeof this.__value === "number") return Matrix(x(this.__value));
return Matrix(this.__value.map(x));
Matrix.prototype.map = function (
this: Matrix,
x: <T extends number | number[]>(value: T, index: number, array: T[]) => T
): Matrix {
if (isMatrix1D(this)) {
return Matrix(this.__value.map(x<number>));
} else {
return Matrix(this.__value.map(x<number[]>));
}
};

/**
* Returns the number or number array value
* @alias module:matrix#valueOf
* @return {number|number[]} Number of number array value
* @return {number[]|number[][]} Number of number array value
*/
Matrix.prototype.valueOf = function (
this: Matrix
): number | (number | number[])[] {
Matrix.prototype.valueOf = function (this: Matrix): number[] | number[][] {
return this.__value;
};

Expand All @@ -264,26 +274,22 @@ Matrix.prototype.valueOf = function (
Matrix.prototype[Symbol.for("nodejs.util.inspect.custom")] = function (
this: Matrix
): string {
switch (this.countRows()) {
case 0:
return `${this.__value}`;
case 1:
return `[ ${(this.__value as number[]).join(" ")} ]`;
default:
/* Output array filled with zeroes */
const padding: number[] = unzip(this.__value as number[][]).map(
(column: number[]) =>
column.reduce((length, x) => Math.max(`${x}`.length, length), 0)
);
return (this.__value as number[][])
.reduce(
(output, row) =>
`${output}[ ${row
.map((x, i) => padStart(`${x}`, padding[i]))
.join(" ")} ]`,
""
)
.replace(/]\[/g, "]\n[");
if (isMatrix1D(this)) {
return `[ ${this.__value.join(" ")} ]`;
} else {
/* Output array filled with zeroes */
const padding: number[] = unzip(this.__value).map((column: number[]) =>
column.reduce((length, x) => Math.max(`${x}`.length, length), 0)
);
return this.__value
.reduce(
(output, row) =>
`${output}[ ${row
.map((x, i) => padStart(`${x}`, padding[i]))
.join(" ")} ]`,
""
)
.replace(/]\[/g, "]\n[");
}
};

Expand Down