diff --git a/README.md b/README.md index 428f436..2508e42 100644 --- a/README.md +++ b/README.md @@ -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]) diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 7cb7d9a..cb9ded0 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -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); }); @@ -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)", () => { @@ -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)", () => { @@ -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); }); @@ -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); }); @@ -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]])); }); @@ -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 ]"); }); diff --git a/src/index.ts b/src/index.ts index 64162a6..2b33883 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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; @@ -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"); } @@ -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); + }) ); }; @@ -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(); @@ -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(y.__value as number[][])) && - unzip(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 + ); } /** @@ -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)); + } + }); + } }; /** @@ -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; }; @@ -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; }; @@ -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)); } }; @@ -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: (value: T, index: number, array: T[]) => T +): Matrix { + if (isMatrix1D(this)) { + return Matrix(this.__value.map(x)); + } else { + return Matrix(this.__value.map(x)); + } }; /** * 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; }; @@ -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["); } };