From b6f31d0e3e08bfc62595e185c5a3ccc99cb510f9 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:58:31 +0100 Subject: [PATCH 01/20] wip: SymmetricMatrix impl --- src/matrix.js | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/src/matrix.js b/src/matrix.js index c98253b..d09b1e5 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -1646,3 +1646,124 @@ export default class Matrix extends AbstractMatrix { } installMathOperations(AbstractMatrix, Matrix); + +export class SymmetricMatrix extends AbstractMatrix { + /** + * not the same as matrix.isSymmetric() + * Here is to check if it's instanceof SymmetricMatrix without bundling issues + * + * @param value + * @returns {boolean} + */ + static isSymmetricMatrix(value) { + return Matrix.isMatrix(value) && value.klassType === 'SymmetricMatrix'; + } + + /** + * upper-left corner flat 1DArray length + * + * 1 2 3 4 + * 0 5 6 7 + * 0 0 8 9 + * 0 0 0 10 + * + * 1DArray flat is [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + * So the length is 10 + * + * @param {number} sideSize + * @returns {number} + * @private + */ + static computeInternalDataLength(sideSize) { + // Summation(i=0, n=sideSize) + return (sideSize * (sideSize + 1)) / 2; + } + + /** + * upper-right matrix corner in 1D flat array + * + * @type {Float64Array} + * @private + */ + data; + + /** + * @public + * @readonly + * @type {number} + */ + sideSize; + + get rows() { + return this.sideSize; + } + get columns() { + return this.sideSize; + } + + constructor(sideSize) { + super(); + + let _sideSize = sideSize; + + if (SymmetricMatrix.isSymmetricMatrix(sideSize)) { + // eslint-disable-next-line no-constructor-return + return sideSize.clone(); + } + if (Matrix.isMatrix(sideSize)) { + if (!(sideSize.isSquare() && sideSize.isSymmetric())) { + throw new TypeError( + 'first argument is a matrix but is not square or is not symmetric', + ); + } + + _sideSize = sideSize.rows; + + this.data = new Float64Array( + SymmetricMatrix.computeInternalDataLength(_sideSize), + ); + for (let col = 0, row = 0, index = 0; index < this.data.length; index++) { + this.data[index] = sideSize.get(row, col); + + if (++col > _sideSize) col = ++row; + } + } else if (Number.isInteger(sideSize) && sideSize >= 0) { + // Create an empty matrix + this.data = new Float64Array( + SymmetricMatrix.computeInternalDataLength(sideSize), + ); + } else if (isAnyArray(sideSize)) { + // Copy the values from the 2D array + const matrix = new Matrix(sideSize); + if (!(matrix.isSquare() && matrix.isSymmetric())) { + throw new TypeError( + 'first argument is a matrix like but is not square or is not symmetric', + ); + } + + _sideSize = matrix.columns; + this.data = new Float64Array( + SymmetricMatrix.computeInternalDataLength(_sideSize), + ); + for (let col = 0, row = 0, index = 0; index < this.data.length; index++) { + this.data[index] = matrix.get(row, col); + + if (++col > _sideSize) col = ++row; + } + } else { + throw new TypeError( + 'First argument must be a Matrix or array of array or positive number', + ); + } + + this.sideSize = _sideSize; + } + + clone() { + const matrix = new SymmetricMatrix(this.sideSize); + matrix.data = this.data.slice(); + + return matrix; + } +} +SymmetricMatrix.prototype.klassType = 'Symmetric'; From 9a643be9cd3eedba76d2753f0163ded39e269272 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Thu, 23 Nov 2023 18:13:14 +0100 Subject: [PATCH 02/20] wip: SymmetricMatrix impl extend Matrix I didn't found a formula to compute compact index from row/column coordinate --- src/matrix.js | 203 +++++++++++++++++++++++++++++++------------------- 1 file changed, 128 insertions(+), 75 deletions(-) diff --git a/src/matrix.js b/src/matrix.js index d09b1e5..0a0235c 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -1533,6 +1533,11 @@ AbstractMatrix.prototype.tensorProduct = AbstractMatrix.prototype.kroneckerProduct; export default class Matrix extends AbstractMatrix { + /** + * @type {Float64Array[]} + */ + data; + constructor(nRows, nColumns) { super(); if (Matrix.isMatrix(nRows)) { @@ -1647,45 +1652,37 @@ export default class Matrix extends AbstractMatrix { installMathOperations(AbstractMatrix, Matrix); -export class SymmetricMatrix extends AbstractMatrix { +/** + * @typedef {0 | 1 | number | boolean} Mask + */ + +export class SymmetricMatrix extends Matrix { /** - * not the same as matrix.isSymmetric() - * Here is to check if it's instanceof SymmetricMatrix without bundling issues - * - * @param value - * @returns {boolean} + * @param {number} sideSize + * @returns {SymmetricMatrix} */ - static isSymmetricMatrix(value) { - return Matrix.isMatrix(value) && value.klassType === 'SymmetricMatrix'; + static zeros(sideSize) { + return new SymmetricMatrix(sideSize); } /** - * upper-left corner flat 1DArray length - * - * 1 2 3 4 - * 0 5 6 7 - * 0 0 8 9 - * 0 0 0 10 - * - * 1DArray flat is [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - * So the length is 10 - * * @param {number} sideSize - * @returns {number} - * @private + * @returns {SymmetricMatrix} */ - static computeInternalDataLength(sideSize) { - // Summation(i=0, n=sideSize) - return (sideSize * (sideSize + 1)) / 2; + static ones(sideSize) { + return new SymmetricMatrix(sideSize).fill(1); } /** - * upper-right matrix corner in 1D flat array + * not the same as matrix.isSymmetric() + * Here is to check if it's instanceof SymmetricMatrix without bundling issues * - * @type {Float64Array} - * @private + * @param value + * @returns {boolean} */ - data; + static isSymmetricMatrix(value) { + return Matrix.isMatrix(value) && value.klassType === 'SymmetricMatrix'; + } /** * @public @@ -1694,74 +1691,130 @@ export class SymmetricMatrix extends AbstractMatrix { */ sideSize; - get rows() { - return this.sideSize; - } - get columns() { - return this.sideSize; - } - constructor(sideSize) { - super(); - - let _sideSize = sideSize; - if (SymmetricMatrix.isSymmetricMatrix(sideSize)) { // eslint-disable-next-line no-constructor-return return sideSize.clone(); } + if (Matrix.isMatrix(sideSize)) { if (!(sideSize.isSquare() && sideSize.isSymmetric())) { throw new TypeError( 'first argument is a matrix but is not square or is not symmetric', ); } + } - _sideSize = sideSize.rows; + if (typeof sideSize === 'number') { + super(sideSize, sideSize); + } else { + super(sideSize); + } - this.data = new Float64Array( - SymmetricMatrix.computeInternalDataLength(_sideSize), - ); - for (let col = 0, row = 0, index = 0; index < this.data.length; index++) { - this.data[index] = sideSize.get(row, col); + this.sideSize = this.rows; + } - if (++col > _sideSize) col = ++row; - } - } else if (Number.isInteger(sideSize) && sideSize >= 0) { - // Create an empty matrix - this.data = new Float64Array( - SymmetricMatrix.computeInternalDataLength(sideSize), - ); - } else if (isAnyArray(sideSize)) { - // Copy the values from the 2D array - const matrix = new Matrix(sideSize); - if (!(matrix.isSquare() && matrix.isSymmetric())) { - throw new TypeError( - 'first argument is a matrix like but is not square or is not symmetric', - ); - } + clone() { + const matrix = Object.create(SymmetricMatrix.prototype); - _sideSize = matrix.columns; - this.data = new Float64Array( - SymmetricMatrix.computeInternalDataLength(_sideSize), - ); - for (let col = 0, row = 0, index = 0; index < this.data.length; index++) { - this.data[index] = matrix.get(row, col); + // eslint-disable-next-line no-multi-assign + matrix.rows = matrix.columns = matrix.sideSize = this.sideSize; + matrix.data = this.data.map((row) => row.slice()); - if (++col > _sideSize) col = ++row; - } - } else { - throw new TypeError( - 'First argument must be a Matrix or array of array or positive number', - ); + return matrix; + } + + set(rowIndex, columnIndex, value) { + // symmetric set + super.set(rowIndex, columnIndex, value); + super.set(columnIndex, rowIndex, value); + + return this; + } + + removeSide(index) { + // symmetric remove side + super.removeRow(index); + super.removeColumn(index); + + return this; + } + + addSide(index, array) { + super.addRow(index, array); + super.addColumn(index, array); + + return this; + } + + removeRow(index) { + return this.removeSide(index); + } + + addRow(index, array) { + return this.addSide(index, array); + } + + removeColumn(index) { + return this.removeSide(index); + } + + addColumn(index, array) { + return this.addSide(index, array); + } + + /** + * @param {Mask[]} mask + */ + applyMask(mask) { + if (mask.length !== this.sideSize) { + throw new RangeError('mask size do not match with matrix size'); + } + + // prepare sides to remove from matrix from mask + /** @type {number[]} */ + const sidesToRemove = []; + for (const [index, passthroughs] of mask.entries()) { + if (passthroughs) continue; + sidesToRemove.push(index); } + // to remove from highest to lowest for no mutation shifting + sidesToRemove.reverse(); - this.sideSize = _sideSize; + // remove sides + for (const sideIndex of sidesToRemove) { + this.removeSide(sideIndex); + } } - clone() { - const matrix = new SymmetricMatrix(this.sideSize); - matrix.data = this.data.slice(); + toCompact() { + const { sideSize } = this; + /** @type {number[]} */ + const compact = new Array((sideSize * (sideSize + 1)) / 2); + for (let col = 0, row = 0, index = 0; index < compact.length; index++) { + compact[index] = this.get(row, col); + + if (++col >= this.sideSize) col = ++row; + } + + return compact; + } + + /** + * @param {number[]} compact + */ + static fromCompact(compact) { + const compactSize = compact.length; + // compactSize = (sideSize * (sideSize + 1)) / 2 + // https://mathsolver.microsoft.com/fr/solve-problem/y%20%3D%20%20x%20%60cdot%20%20%20%60frac%7B%20%20%60left(%20x%2B1%20%20%60right)%20%20%20%20%7D%7B%202%20%20%7D + // sideSize = (Sqrt(8 × compactSize + 1) - 1) / 2 + const sideSize = (Math.sqrt(8 * compactSize + 1) - 1) / 2; + + const matrix = new SymmetricMatrix(sideSize); + for (let col = 0, row = 0, index = 0; index < compactSize; index++) { + matrix.set(col, row); + if (++col >= sideSize) col = ++row; + } return matrix; } From ee25b8ad4d39a39985004cc38e8f66949a96b70a Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Fri, 24 Nov 2023 10:14:54 +0100 Subject: [PATCH 03/20] feat: implement Distance and Symmetric matrix --- src/matrix.js | 130 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 121 insertions(+), 9 deletions(-) diff --git a/src/matrix.js b/src/matrix.js index 0a0235c..7a4ff52 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -240,6 +240,16 @@ export class AbstractMatrix { return false; } + isDistance() { + if (!this.isSymmetric()) return false; + + for (let i = 0; i < this.rows; i++) { + if (this.get(i, i) !== 0) return false; + } + + return true; + } + isEchelonForm() { let i = 0; let j = 0; @@ -1697,25 +1707,26 @@ export class SymmetricMatrix extends Matrix { return sideSize.clone(); } - if (Matrix.isMatrix(sideSize)) { - if (!(sideSize.isSquare() && sideSize.isSymmetric())) { - throw new TypeError( - 'first argument is a matrix but is not square or is not symmetric', - ); - } - } - if (typeof sideSize === 'number') { super(sideSize, sideSize); } else { super(sideSize); + + if (!this.isSymmetric()) { + throw new TypeError('not symmetric data'); + } } this.sideSize = this.rows; } clone() { - const matrix = Object.create(SymmetricMatrix.prototype); + /* + * Optimized matrix cloning support inheritance + * create with current prototype and add data + * skip constructor checks and full-scan iterations + */ + const matrix = Object.create(this.constructor.prototype); // eslint-disable-next-line no-multi-assign matrix.rows = matrix.columns = matrix.sideSize = this.sideSize; @@ -1724,6 +1735,10 @@ export class SymmetricMatrix extends Matrix { return matrix; } + toMatrix() { + return new Matrix(this); + } + set(rowIndex, columnIndex, value) { // symmetric set super.set(rowIndex, columnIndex, value); @@ -1787,8 +1802,27 @@ export class SymmetricMatrix extends Matrix { } } + /** + * Compact format upper-right corner of matrix + * iterable from left to right, from top to bottom. + * + * ``` + * A B C D + * A 1 2 3 4 + * B 2 5 6 7 + * C 3 6 8 9 + * D 4 7 9 10 + * ``` + * + * will return compact 1D array `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]` + * + * length is S(i=0, n=sideSize) => 10 for a 4 sideSized matrix + * + * @returns {number[]} + */ toCompact() { const { sideSize } = this; + /** @type {number[]} */ const compact = new Array((sideSize * (sideSize + 1)) / 2); for (let col = 0, row = 0, index = 0; index < compact.length; index++) { @@ -1810,6 +1844,14 @@ export class SymmetricMatrix extends Matrix { // sideSize = (Sqrt(8 × compactSize + 1) - 1) / 2 const sideSize = (Math.sqrt(8 * compactSize + 1) - 1) / 2; + if (!Number.isInteger(sideSize)) { + throw new TypeError( + `this array is not a compact representation of a Symmetric, ${JSON.stringify( + compact, + )}`, + ); + } + const matrix = new SymmetricMatrix(sideSize); for (let col = 0, row = 0, index = 0; index < compactSize; index++) { matrix.set(col, row); @@ -1820,3 +1862,73 @@ export class SymmetricMatrix extends Matrix { } } SymmetricMatrix.prototype.klassType = 'Symmetric'; + +export class DistanceMatrix extends SymmetricMatrix { + constructor(sideSize) { + super(sideSize); + + if (!this.isDistance()) { + throw new Error('provided arguments do no produce a distance matrix'); + } + } + + /** + * Compact format upper-right corner of matrix + * no diagonal (only zeros) + * iterable from left to right, from top to bottom. + * + * ``` + * A B C D + * A 0 1 2 3 + * B 1 0 4 5 + * C 2 4 0 6 + * D 3 5 6 0 + * ``` + * + * will return compact 1D array `[1, 2, 3, 4, 5, 6]` + * + * length is S(i=0, n=sideSize-1) => 6 for a 4 side sized matrix + * + * @returns {number[]} + */ + toCompact() { + const { sideSize } = this; + const compactLength = ((sideSize - 1) * sideSize) / 2; + + /** @type {number[]} */ + const compact = new Array(compactLength); + for (let col = 0, row = 0, index = 0; index < compact.length; index++) { + compact[index] = this.get(row, col); + + if (++col >= this.sideSize) col = ++row; + } + + return compact; + } + + /** + * @param {number[]} compact + */ + static fromCompact(compact) { + const compactSize = compact.length; + // compactSize = (sideSize * (sideSize - 1)) / 2 + // sideSize = (Sqrt(8 × compactSize + 1) + 1) / 2 + const sideSize = (Math.sqrt(8 * compactSize + 1) + 1) / 2; + + if (!Number.isInteger(sideSize)) { + throw new TypeError( + `this array is not a compact representation of a DistanceMatrix, ${JSON.stringify( + compact, + )}`, + ); + } + + const matrix = new SymmetricMatrix(sideSize); + for (let col = 1, row = 0, index = 0; index < compactSize; index++) { + matrix.set(col, row); + if (++col >= sideSize) col = ++row + 1; + } + + return matrix; + } +} From 50a060412a7be4fb59195efe55f0d3fa29c53be8 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Fri, 24 Nov 2023 12:57:41 +0100 Subject: [PATCH 04/20] feat: add typings --- matrix.d.ts | 207 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/matrix.js | 69 +++++++++-------- 2 files changed, 245 insertions(+), 31 deletions(-) diff --git a/matrix.d.ts b/matrix.d.ts index a7c2c8b..3c0e045 100644 --- a/matrix.d.ts +++ b/matrix.d.ts @@ -1,6 +1,11 @@ type MaybeMatrix = AbstractMatrix | ArrayLike>; type ScalarOrMatrix = number | MaybeMatrix; type MatrixDimension = 'row' | 'column'; +type MaskValue = 0 | 1 | number | boolean; +/** + * allow use of numbers (only 0 is considered false) and booleans + */ +type Mask = MaskValue[]; export interface IRandomOptions { /** @@ -195,6 +200,7 @@ export abstract class AbstractMatrix { * Creates a matrix with the given dimensions. Values will be random integers. * @param rows - Number of rows. * @param columns - Number of columns. + * @param options * @returns - The new matrix. */ static randInt( @@ -971,6 +977,207 @@ export class Matrix extends AbstractMatrix { export default Matrix; +export class SymmetricMatrix extends Matrix { + /** + * Creates a symmetric matrix with the given dimensions. Values will be set to zero. + * This is equivalent to calling the Matrix constructor. + * @param sidesSize - Number of rows or columns (square). + * @returns The new symmetric matrix. + */ + static zeros(sidesSize: number): SymmetricMatrix; + + /** + * Creates a symmetric matrix with the given dimensions. Values will be set to one. + * @param sidesSize - Number of rows or columns (square). + * @returns The new symmetric matrix. + */ + static ones(sidesSize: number): SymmetricMatrix; + + static isSymmetricMatrix(value: unknown): value is SymmetricMatrix; + + /** + * alias for `rows` or `columns` (square matrix so equals) + */ + get sideSize(): number; + + constructor(sidesSize: number); + /** + * @throws TypeError if data are not symmetric + * @param data + */ + constructor(data: ArrayLike>); + /** + * @throws TypeError if otherMatrix is not symmetric + * @param otherMatrix + */ + constructor(otherMatrix: AbstractMatrix); + + /** + * copy to a new matrix + */ + toMatrix(): Matrix; + + /** + * Symmetric remove row / column + * @param index + */ + removeSide(index: number): this; + + /** + * Symmetric add row / column + * @param index + * @param array + */ + addSide(index: number, array: ArrayLike | AbstractMatrix): this; + + /** + * alias to removeSide + * @param index + */ + removeRow(index: number): this; + /** + * alias to addSide + * @param index + * @param array + */ + addRow(index: number, array: ArrayLike | AbstractMatrix): this; + /** + * alias to removeSide + * @param index + */ + removeColumn(index: number): this; + /** + * alias to addSide + * @param index + * @param array + */ + addColumn(index: number, array: ArrayLike | AbstractMatrix): this; + + /** + * remove sides (rows / columns) with falsy value from mask. + * + * @example + * + * ```js + * const matrix = new SymmetricMatrix([ + * [0,1,2,3], + * [1,0,4,5], + * [2,4,0,6], + * [3,5,6,0], + * ]); + * matrix.applyMask([1,0,0,1]); + * assert.deepEqual(matrix.toCompact(), new SymmetricMatrix([ + * [0,3], + * [3,0], + * ]).toCompact()); + * ``` + * + * @throws RangeError if mask length is different of matrix sideSize + * + * @param mask + */ + applyMask(mask: Mask): this; + + /** + * Compact format upper-right corner of matrix + * iterate from left to right, from top to bottom. + * + * ``` + * full view | usefull data + * A B C D | A B C D + * A 1 2 3 4 | A 1 2 3 4 + * B 2 5 6 7 | B · 5 6 7 + * C 3 6 8 9 | C · · 8 9 + * D 4 7 9 10 | D · · · 10 + * ``` + * + * will return compact 1D array `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]` + * + * length is S(i=0, n=sideSize) => 10 for a 4 sideSized matrix + */ + toCompact(): number[]; + + /** + * @throws TypeError if `compact` is not the compact form of a Symmetric Matrix + * `(Math.sqrt(8 * compactLength + 1) - 1) / 2` must be an integer + * + * @param compact + */ + static fromCompact(compact: number[]): SymmetricMatrix; +} + +export class DistanceMatrix extends SymmetricMatrix { + /** + * Creates a distance matrix with the given dimensions. Values will be set to zero. + * This is equivalent to calling the Matrix constructor. + * @param sidesSize - Number of rows or columns (square). + * @returns The new symmetric matrix. + */ + static zeros(sidesSize: number): DistanceMatrix; + + /** + * Creates a symmetric matrix with the given dimensions. Values will be set to one. + * @param sidesSize - Number of rows or columns (square). + * @returns The new symmetric matrix. + */ + static ones(sidesSize: number): DistanceMatrix; + + static isDistanceMatrix(value: unknown): value is DistanceMatrix; + + constructor(sidesSize: number); + /** + * @throws TypeError if data are not symmetric and diagonal is not 0 + * @param data + */ + constructor(data: ArrayLike>); + /** + * @throws TypeError if otherMatrix is not symmetric and diagonal is not 0 + * @param otherMatrix + */ + constructor(otherMatrix: AbstractMatrix); + + /** + * because it's a distance matrix, if rowIndex === columnIndex, + * value will be set to 0 + * + * @param rowIndex + * @param columnIndex + * @param value + */ + set(rowIndex: number, columnIndex: number, value: number): this; + + toSymmetricMatrix(): SymmetricMatrix; + + /** + * Compact format upper-right corner of matrix + * no diagonal (because only store zeros) + * iterable from left to right, from top to bottom. + * + * ``` + * A B C D + * A 0 1 2 3 + * B 1 0 4 5 + * C 2 4 0 6 + * D 3 5 6 0 + * ``` + * + * will return compact 1D array `[1, 2, 3, 4, 5, 6]` + * + * length is S(i=0, n=sideSize-1) => 6 for a 4 side sized matrix + * + * @returns {number[]} + */ + toCompact(): number[]; + + /** + * @throws TypeError if `compact` is not the compact form of a Distance Matrix + * `(Math.sqrt(8 * compactSize + 1) + 1) / 2` must be an integer + * + * @param compact + */ + static fromCompact(compact: number[]): DistanceMatrix; +} + export class MatrixColumnView extends AbstractMatrix { constructor(matrix: AbstractMatrix, column: number); set(rowIndex: number, columnIndex: number, value: number): this; diff --git a/src/matrix.js b/src/matrix.js index 7a4ff52..effc47b 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -66,11 +66,11 @@ export class AbstractMatrix { } static zeros(rows, columns) { - return new Matrix(rows, columns); + return new this(rows, columns); } static ones(rows, columns) { - return new Matrix(rows, columns).fill(1); + return new this(rows, columns).fill(1); } static rand(rows, columns, options = {}) { @@ -1667,22 +1667,6 @@ installMathOperations(AbstractMatrix, Matrix); */ export class SymmetricMatrix extends Matrix { - /** - * @param {number} sideSize - * @returns {SymmetricMatrix} - */ - static zeros(sideSize) { - return new SymmetricMatrix(sideSize); - } - - /** - * @param {number} sideSize - * @returns {SymmetricMatrix} - */ - static ones(sideSize) { - return new SymmetricMatrix(sideSize).fill(1); - } - /** * not the same as matrix.isSymmetric() * Here is to check if it's instanceof SymmetricMatrix without bundling issues @@ -1694,12 +1678,9 @@ export class SymmetricMatrix extends Matrix { return Matrix.isMatrix(value) && value.klassType === 'SymmetricMatrix'; } - /** - * @public - * @readonly - * @type {number} - */ - sideSize; + get sideSize() { + return this.rows; + } constructor(sideSize) { if (SymmetricMatrix.isSymmetricMatrix(sideSize)) { @@ -1716,8 +1697,6 @@ export class SymmetricMatrix extends Matrix { throw new TypeError('not symmetric data'); } } - - this.sideSize = this.rows; } clone() { @@ -1729,7 +1708,7 @@ export class SymmetricMatrix extends Matrix { const matrix = Object.create(this.constructor.prototype); // eslint-disable-next-line no-multi-assign - matrix.rows = matrix.columns = matrix.sideSize = this.sideSize; + matrix.rows = matrix.columns = this.sideSize; matrix.data = this.data.map((row) => row.slice()); return matrix; @@ -1800,11 +1779,13 @@ export class SymmetricMatrix extends Matrix { for (const sideIndex of sidesToRemove) { this.removeSide(sideIndex); } + + return this; } /** * Compact format upper-right corner of matrix - * iterable from left to right, from top to bottom. + * iterate from left to right, from top to bottom. * * ``` * A B C D @@ -1861,17 +1842,42 @@ export class SymmetricMatrix extends Matrix { return matrix; } } -SymmetricMatrix.prototype.klassType = 'Symmetric'; +SymmetricMatrix.prototype.klassType = 'SymmetricMatrix'; export class DistanceMatrix extends SymmetricMatrix { + /** + * not the same as matrix.isSymmetric() + * Here is to check if it's instanceof SymmetricMatrix without bundling issues + * + * @param value + * @returns {boolean} + */ + static isDistanceMatrix(value) { + return ( + SymmetricMatrix.isSymmetricMatrix(value) && + value.klassSubType === 'DistanceMatrix' + ); + } + constructor(sideSize) { super(sideSize); if (!this.isDistance()) { - throw new Error('provided arguments do no produce a distance matrix'); + throw new TypeError('provided arguments do no produce a distance matrix'); } } + set(rowIndex, columnIndex, value) { + // distance matrix diagonal is 0 + if (rowIndex === columnIndex) value = 0; + + return super.set(rowIndex, columnIndex, value); + } + + toSymmetricMatrix() { + return new SymmetricMatrix(this); + } + /** * Compact format upper-right corner of matrix * no diagonal (only zeros) @@ -1923,7 +1929,7 @@ export class DistanceMatrix extends SymmetricMatrix { ); } - const matrix = new SymmetricMatrix(sideSize); + const matrix = new this(sideSize); for (let col = 1, row = 0, index = 0; index < compactSize; index++) { matrix.set(col, row); if (++col >= sideSize) col = ++row + 1; @@ -1932,3 +1938,4 @@ export class DistanceMatrix extends SymmetricMatrix { return matrix; } } +DistanceMatrix.prototype.klassSubType = 'DistanceMatrix'; From 449c1f0a7785c2fba6fe3fb2e5a529b656d8b1d5 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Fri, 24 Nov 2023 15:23:12 +0100 Subject: [PATCH 05/20] feat: add matrix iterators --- matrix.d.ts | 54 ++++++++++++++++++++++++++ src/matrix.js | 105 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 157 insertions(+), 2 deletions(-) diff --git a/matrix.d.ts b/matrix.d.ts index 3c0e045..a7ebc9b 100644 --- a/matrix.d.ts +++ b/matrix.d.ts @@ -830,6 +830,60 @@ export abstract class AbstractMatrix { toString(options?: IToStringOptions): string; + // iterators methods + + /** + * iterator from left to right, from top to bottom + * yield [row, column, value] + */ + [Symbol.iterator](): Generator< + [row: number, column: number, value: number], + void, + never + >; + + /** + * iterator from left to right, from top to bottom + * yield [row, column, value] + */ + entries(): Generator< + [row: number, column: number, value: number], + void, + never + >; + + /** + * iterator from left to right, from top to bottom + * yield value + */ + values(): Generator; + + /** + * half iterator upper-right-corner from left to right, from top to bottom + * yield [row, column, value] + * @param [borderMax=this.rows] - clamp between Max(rows, columns) and 0. + * @param [missValue=0] + */ + upperRightEntries( + borderMax?: number, + missValue?: Miss | ((this: this, row: number, column: number) => Miss), + ): Generator< + [row: number, column: number, value: number | Miss], + void, + never + >; + + /** + * half iterator upper-right-corner from left to right, from top to bottom + * yield value + * @param [borderMax=this.rows] - clamp between Max(rows, columns) and 0. + * @param [missValue=0] + */ + upperRightValues( + borderMax?: number, + missValue?: Miss | ((this: this, row: number, column: number) => Miss), + ): Generator; + // From here we document methods dynamically generated from operators // Mathematical operators diff --git a/src/matrix.js b/src/matrix.js index effc47b..b182b47 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -66,11 +66,11 @@ export class AbstractMatrix { } static zeros(rows, columns) { - return new this(rows, columns); + return new Matrix(rows, columns); } static ones(rows, columns) { - return new this(rows, columns).fill(1); + return new Matrix(rows, columns).fill(1); } static rand(rows, columns, options = {}) { @@ -1514,6 +1514,99 @@ export class AbstractMatrix { toString(options) { return inspectMatrixWithOptions(this, options); } + + [Symbol.iterator]() { + return this.entries(); + } + + /** + * iterator from left to right, from top to bottom + * yield [row, column, value] + * @returns {Generator<[number, number, number], void, *>} + */ + *entries() { + for (let row = 0; row < this.rows; row++) { + for (let col = 0; col < this.columns; col++) { + yield [row, col, this.get(row, col)]; + } + } + } + + /** + * iterator from left to right, from top to bottom + * yield value + * @returns {Generator} + */ + *values() { + for (let row = 0; row < this.rows; row++) { + for (let col = 0; col < this.columns; col++) { + yield this.get(row, col); + } + } + } + + /** + * half iterator upper-right-corner from left to right, from top to bottom + * yield [row, column, value] + * + * @param {number} [borderMax = this.rows] - if specified will check if <= to rows and columns + * @param [missValue = 0] - number or function returning a number + * + * @returns {Generator<[number, number, number], void, *>} + */ + *upperRightEntries(borderMax = this.rows, missValue = 0) { + borderMax = Math.min( + Math.max(this.rows, this.columns), + Math.max(0, borderMax), + ); + + const isMissValueFunction = typeof missValue?.call === 'function'; + + for (let row = 0, col = 0; row < borderMax; void 0) { + const value = + row >= this.rows || col >= this.columns + ? isMissValueFunction + ? missValue.call(this, row, col) + : missValue + : this.get(row, col); + + yield [row, col, value]; + + // at the end of row, move cursor to next row at diagonal position + if (++col >= borderMax) col = ++row; + } + } + + /** + * half iterator upper-right-corner from left to right, from top to bottom + * yield value + * + * @param {number} [borderMax = this.rows] - if specified will check if <= to rows and columns + * @param [missValue = 0] - number or function returning a number + * + * @returns {Generator<[number, number, number], void, *>} + */ + *upperRightValues(borderMax = this.rows, missValue = 0) { + borderMax = Math.min( + Math.max(this.rows, this.columns), + Math.max(0, borderMax), + ); + const isMissValueFunction = typeof missValue?.call === 'function'; + + for (let row = 0, col = 0; row < borderMax; void 0) { + const value = + row >= this.rows || col >= this.columns + ? isMissValueFunction + ? missValue.call(this, row, col) + : missValue + : this.get(row, col); + + yield value; + + // at the end of row, move cursor to next row at diagonal position + if (++col >= borderMax) col = ++row; + } + } } AbstractMatrix.prototype.klass = 'Matrix'; @@ -1678,6 +1771,14 @@ export class SymmetricMatrix extends Matrix { return Matrix.isMatrix(value) && value.klassType === 'SymmetricMatrix'; } + static zeros(rows, columns) { + return new this(rows, columns); + } + + static ones(rows, columns) { + return new this(rows, columns).fill(1); + } + get sideSize() { return this.rows; } From 36d51be2cae0e7b2045b6a1018bf9298537bdf49 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Fri, 24 Nov 2023 16:55:00 +0100 Subject: [PATCH 06/20] test: add matrix iterators tests --- src/__tests__/matrix/iterators.test.js | 169 +++++++++++++++++++++++++ src/__tests__/matrix/utility.test.js | 38 ++++++ src/matrix.js | 2 +- 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/matrix/iterators.test.js diff --git a/src/__tests__/matrix/iterators.test.js b/src/__tests__/matrix/iterators.test.js new file mode 100644 index 0000000..4444c08 --- /dev/null +++ b/src/__tests__/matrix/iterators.test.js @@ -0,0 +1,169 @@ +import { describe, it, expect } from 'vitest'; + +import Matrix from '../../matrix'; + +describe('iterators methods', () => { + const square = new Matrix([ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + ]); + + const rect = new Matrix([ + [0, 1, 2, 3], + [4, 5, 6, 7], + ]); + + it('entries', () => { + expect(Array.from(square.entries())).toStrictEqual([ + [0, 0, 0], + [0, 1, 1], + [0, 2, 2], + [1, 0, 3], + [1, 1, 4], + [1, 2, 5], + [2, 0, 6], + [2, 1, 7], + [2, 2, 8], + ]); + expect(Array.from(rect.entries())).toStrictEqual([ + [0, 0, 0], + [0, 1, 1], + [0, 2, 2], + [0, 3, 3], + [1, 0, 4], + [1, 1, 5], + [1, 2, 6], + [1, 3, 7], + ]); + }); + it('Symbol.iterator is same as entries', () => { + expect(Array.from(square)).toStrictEqual(Array.from(square.entries())); + expect(Array.from(rect)).toStrictEqual(Array.from(rect.entries())); + }); + it('values', () => { + expect(Array.from(square.values())).toStrictEqual([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, + ]); + expect(Array.from(rect.values())).toStrictEqual([0, 1, 2, 3, 4, 5, 6, 7]); + }); + + it('upperRightEntries simple', () => { + expect(Array.from(square.upperRightEntries())).toStrictEqual([ + [0, 0, 0], + [0, 1, 1], + [0, 2, 2], + [1, 1, 4], + [1, 2, 5], + [2, 2, 8], + ]); + + // on no borderMax, fallback on number of rows, so on a wide rectangle, it reduce read to inset left-up square matrix + expect(Array.from(rect.upperRightEntries())).toStrictEqual([ + [0, 0, 0], + [0, 1, 1], + [1, 1, 5], + ]); + }); + it('upperRightEntries expand columns', () => { + expect(Array.from(rect.upperRightEntries(rect.columns))).toStrictEqual([ + [0, 0, 0], + [0, 1, 1], + [0, 2, 2], + [0, 3, 3], + [1, 1, 5], + [1, 2, 6], + [1, 3, 7], + [2, 2, 0], + [2, 3, 0], + [3, 3, 0], + ]); + }); + it('upperRightEntries expand columns with default values', () => { + const value = Array.from(rect.upperRightEntries(rect.columns, '∅')); + const expected = [ + [0, 0, 0], + [0, 1, 1], + [0, 2, 2], + [0, 3, 3], + [1, 1, 5], + [1, 2, 6], + [1, 3, 7], + [2, 2, '∅'], + [2, 3, '∅'], + [3, 3, '∅'], + ]; + + expect(value).toStrictEqual(expected); + }); + it('upperRightEntries expand columns with computed default values', () => { + const value = Array.from( + rect.upperRightEntries( + rect.columns, + function index1DVirtualColumnExpand(row, col) { + return row * this.columns + col; + }, + ), + ); + const expected = [ + [0, 0, 0], + [0, 1, 1], + [0, 2, 2], + [0, 3, 3], + [1, 1, 5], + [1, 2, 6], + [1, 3, 7], + [2, 2, 10], + [2, 3, 11], + [3, 3, 15], + ]; + + expect(value).toStrictEqual(expected); + }); + it('upperRightEntries square expand between rows and columns', () => { + const value = Array.from(rect.upperRightEntries(3)); + const expected = [ + [0, 0, 0], + [0, 1, 1], + [0, 2, 2], + [1, 1, 5], + [1, 2, 6], + [2, 2, 0], + ]; + + expect(value).toStrictEqual(expected); + }); + + it('upperRightValues simple', () => { + expect(Array.from(square.upperRightValues())).toStrictEqual([ + 0, 1, 2, 4, 5, 8, + ]); + + expect(Array.from(rect.upperRightValues())).toStrictEqual([0, 1, 5]); + }); + it('upperRightValues expand columns with default values', () => { + const value = Array.from(rect.upperRightValues(rect.columns, '∅')); + const expected = [0, 1, 2, 3, 5, 6, 7, '∅', '∅', '∅']; + + expect(value).toStrictEqual(expected); + }); + it('upperRightValues expand columns with computed default values', () => { + const value = Array.from( + rect.upperRightValues( + rect.columns, + function index1DVirtualColumnExpand(row, col) { + return row * this.columns + col; + }, + ), + ); + const expected = [0, 1, 2, 3, 5, 6, 7, 10, 11, 15]; + + expect(value).toStrictEqual(expected); + }); + it('upperRightValues square expand between rows and columns', () => { + const value = Array.from(rect.upperRightValues(3)); + const expected = [0, 1, 2, 5, 6, 0]; + + expect(value).toStrictEqual(expected); + }); +}); diff --git a/src/__tests__/matrix/utility.test.js b/src/__tests__/matrix/utility.test.js index ecde4b4..7bf7b50 100644 --- a/src/__tests__/matrix/utility.test.js +++ b/src/__tests__/matrix/utility.test.js @@ -550,6 +550,44 @@ describe('utility methods', () => { expect(m.isSymmetric()).toBe(false); }); + it('isDistance', () => { + expect( + new Matrix([ + [1, 0, 2], + [0, 4, 9], + [2, 9, 3], + ]).isDistance(), + ).toBe(false); + expect( + new Matrix([ + [1, 0, 4], + [0, 4, 1], + [2, 9, 3], + ]).isDistance(), + ).toBe(false); + expect( + new Matrix([ + [1, 0, 2], + [0, 4, 9], + ]).isDistance(), + ).toBe(false); + + expect( + new Matrix([ + [0, 1, 2], + [1, 0, 9], + [2, 9, 0], + ]).isDistance(), + ).toBe(true); + expect( + new Matrix([ + [0, 1, 2], + [1, 1, 9], + [2, 9, 0], + ]).isDistance(), + ).toBe(false); + }); + it('isEmpty', () => { expect(new Matrix(0, 0).isEmpty()).toBe(true); expect(new Matrix(0, 1).isEmpty()).toBe(true); diff --git a/src/matrix.js b/src/matrix.js index b182b47..99f4cb8 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -1550,7 +1550,7 @@ export class AbstractMatrix { * yield [row, column, value] * * @param {number} [borderMax = this.rows] - if specified will check if <= to rows and columns - * @param [missValue = 0] - number or function returning a number + * @param {any} [missValue = 0] - number or function returning a number * * @returns {Generator<[number, number, number], void, *>} */ From 92185fc59db9eadffa295af76a563413257de9b3 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Fri, 24 Nov 2023 17:05:10 +0100 Subject: [PATCH 07/20] wip: test for symmetric matrix --- src/__tests__/symmetric_matrix/creation.test.js | 17 +++++++++++++++++ src/index.js | 8 +++++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/symmetric_matrix/creation.test.js diff --git a/src/__tests__/symmetric_matrix/creation.test.js b/src/__tests__/symmetric_matrix/creation.test.js new file mode 100644 index 0000000..ad26f82 --- /dev/null +++ b/src/__tests__/symmetric_matrix/creation.test.js @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest'; + +import { SymmetricMatrix } from '../../index'; + +describe('SymmetricMatrix creation', () => { + it('should create a new object', () => { + const payload = [ + [1, 2, 3], + [2, 4, 5], + [3, 5, 6], + ]; + const matrix = new SymmetricMatrix(payload); + expect(matrix).not.toBe(payload); + expect(matrix).toBeInstanceOf(SymmetricMatrix); + expect(SymmetricMatrix.isSymmetricMatrix(matrix)).toBe(true); + }); +}); diff --git a/src/index.js b/src/index.js index 9cc99a9..3d19797 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,10 @@ -export { AbstractMatrix, default, default as Matrix } from './matrix'; +export { + AbstractMatrix, + default, + default as Matrix, + SymmetricMatrix, + DistanceMatrix, +} from './matrix'; export * from './views/index'; export { wrap } from './wrap/wrap'; From 1a5025a3927d51e6ce0ebf3eca0595431cd94c3b Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Mon, 27 Nov 2023 12:52:40 +0100 Subject: [PATCH 08/20] test: finalize symmetric matrix tests --- matrix.d.ts | 5 + .../symmetric_matrix/creation.test.js | 147 +++++++++++++++++- src/__tests__/symmetric_matrix/export.test.js | 15 ++ .../symmetric_matrix/mutations.test.js | 103 ++++++++++++ src/matrix.js | 52 ++++--- 5 files changed, 298 insertions(+), 24 deletions(-) create mode 100644 src/__tests__/symmetric_matrix/export.test.js create mode 100644 src/__tests__/symmetric_matrix/mutations.test.js diff --git a/matrix.d.ts b/matrix.d.ts index a7ebc9b..a172fe1 100644 --- a/matrix.d.ts +++ b/matrix.d.ts @@ -1020,6 +1020,7 @@ export class Matrix extends AbstractMatrix { * @param array - Column to add. */ addColumn(index: number, array: ArrayLike | AbstractMatrix): this; + addColumn(array: ArrayLike | AbstractMatrix): this; /** * Adds a new row to the matrix (in place). @@ -1027,6 +1028,7 @@ export class Matrix extends AbstractMatrix { * @param array - Row to add. */ addRow(index: number, array: ArrayLike | AbstractMatrix): this; + addRow(array: ArrayLike | AbstractMatrix): this; } export default Matrix; @@ -1083,6 +1085,7 @@ export class SymmetricMatrix extends Matrix { * @param array */ addSide(index: number, array: ArrayLike | AbstractMatrix): this; + addSide(array: ArrayLike | AbstractMatrix): this; /** * alias to removeSide @@ -1095,6 +1098,7 @@ export class SymmetricMatrix extends Matrix { * @param array */ addRow(index: number, array: ArrayLike | AbstractMatrix): this; + addRow(array: ArrayLike | AbstractMatrix): this; /** * alias to removeSide * @param index @@ -1106,6 +1110,7 @@ export class SymmetricMatrix extends Matrix { * @param array */ addColumn(index: number, array: ArrayLike | AbstractMatrix): this; + addColumn(array: ArrayLike | AbstractMatrix): this; /** * remove sides (rows / columns) with falsy value from mask. diff --git a/src/__tests__/symmetric_matrix/creation.test.js b/src/__tests__/symmetric_matrix/creation.test.js index ad26f82..06966e2 100644 --- a/src/__tests__/symmetric_matrix/creation.test.js +++ b/src/__tests__/symmetric_matrix/creation.test.js @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { SymmetricMatrix } from '../../index'; +import { Matrix, SymmetricMatrix } from '../../index'; describe('SymmetricMatrix creation', () => { it('should create a new object', () => { @@ -14,4 +14,149 @@ describe('SymmetricMatrix creation', () => { expect(matrix).toBeInstanceOf(SymmetricMatrix); expect(SymmetricMatrix.isSymmetricMatrix(matrix)).toBe(true); }); + + it('should work with a typed array', () => { + const array = [ + Float64Array.of(1, 2, 3), + Float64Array.of(2, 5, 6), + Float64Array.of(3, 6, 9), + ]; + const matrix = new SymmetricMatrix(array); + expect(matrix.to2DArray()).toStrictEqual([ + [1, 2, 3], + [2, 5, 6], + [3, 6, 9], + ]); + }); + + it('should clone existing symmetric matrix', () => { + const original = new SymmetricMatrix([ + [1, 2, 3], + [2, 4, 5], + [3, 5, 6], + ]); + const matrix = new SymmetricMatrix(original); + expect(matrix).not.toBe(original); + expect(matrix).toBeInstanceOf(SymmetricMatrix); + expect(SymmetricMatrix.isSymmetricMatrix(matrix)).toBe(true); + expect(matrix).toStrictEqual(original); + }); + + it('should create a zero symmetric matrix', () => { + const matrix = new SymmetricMatrix(9); + expect(matrix.rows).toBe(9); + expect(matrix.columns).toBe(9); + expect(matrix.get(0, 0)).toBe(0); + }); + + it('should create an empty symmetric matrix', () => { + const matrix00 = new SymmetricMatrix(0, 0); + expect(matrix00.rows).toBe(0); + expect(matrix00.columns).toBe(0); + }); + + it('should throw with wrong arguments', () => { + expect(() => new SymmetricMatrix(-1)).toThrow( + /^First argument must be a positive number or an array/, + ); + expect(() => new SymmetricMatrix([0, 1, 2, 3])).toThrow( + /^Data must be a 2D array/, + ); + expect( + () => + new SymmetricMatrix([ + [0, 1], + [0, 1, 2], + ]), + ).toThrow(/^Inconsistent array dimensions$/); + expect(() => new SymmetricMatrix()).toThrow( + /^First argument must be a positive number or an array$/, + ); + expect( + () => + new SymmetricMatrix([ + [1, 2, 3], + [4, 5, 6], + [7, undefined, 9], + ]), + ).toThrow(/^Input data contains non-numeric values$/); + expect( + () => + new SymmetricMatrix([ + [0, 1, 2], + [2, 1, 3], + ]), + ).toThrow('not symmetric data'); + expect( + () => + new SymmetricMatrix([ + [0, 1, 2], + [1, 1, 2], + [3, 2, 1], + ]), + ).toThrow('not symmetric data'); + expect( + () => + new SymmetricMatrix( + new Matrix([ + [0, 1, 2], + [1, 1, 2], + [3, 2, 1], + ]), + ), + ).toThrow('not symmetric data'); + }); + + it('should correctly set rows, columns and values', () => { + const matrix = new SymmetricMatrix([ + [5, 9, 4], + [9, 6, 7], + [4, 7, 1], + ]); + expect(matrix.rows).toBe(3); + expect(matrix.columns).toBe(3); + expect(matrix.sideSize).toBe(3); + expect(matrix.get(1, 2)).toBe(7); + }); + + it('should create a symmetric matrix from compact 1D array', () => { + const matrix = SymmetricMatrix.fromCompact([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + expect(matrix.sideSize).toBe(4); + expect(matrix).toStrictEqual( + new SymmetricMatrix([ + [0, 1, 2, 3], + [1, 4, 5, 6], + [2, 5, 7, 8], + [3, 6, 8, 9], + ]), + ); + expect(() => + SymmetricMatrix.fromCompact([0, 1, 2, 3, 4, 5, 6, 8, 9]), + ).toThrow( + /^This array is not a compact representation of a Symmetric Matrix/, + ); + }); + + it('zeros', () => { + expect(SymmetricMatrix.zeros(3).to2DArray()).toStrictEqual([ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + ]); + }); + + it('ones', () => { + expect(SymmetricMatrix.ones(3).to2DArray()).toStrictEqual([ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1], + ]); + }); + + it('copy into a matrix', () => { + const sMatrix = SymmetricMatrix.zeros(3); + const matrix = sMatrix.toMatrix(); + expect(matrix).not.toBeInstanceOf(SymmetricMatrix); + expect(matrix).toBeInstanceOf(Matrix); + }); }); diff --git a/src/__tests__/symmetric_matrix/export.test.js b/src/__tests__/symmetric_matrix/export.test.js new file mode 100644 index 0000000..b5d3e3a --- /dev/null +++ b/src/__tests__/symmetric_matrix/export.test.js @@ -0,0 +1,15 @@ +import { describe, it, expect } from 'vitest'; + +import { SymmetricMatrix } from '../../index'; + +describe('Symmetric export', () => { + it('toCompact', () => { + const matrix = new SymmetricMatrix([ + [0, 1, 2, 3], + [1, 1, 2, 3], + [2, 2, 2, 3], + [3, 3, 3, 3], + ]); + expect(matrix.toCompact()).toStrictEqual([0, 1, 2, 3, 1, 2, 3, 2, 3, 3]); + }); +}); diff --git a/src/__tests__/symmetric_matrix/mutations.test.js b/src/__tests__/symmetric_matrix/mutations.test.js new file mode 100644 index 0000000..b3bbdb3 --- /dev/null +++ b/src/__tests__/symmetric_matrix/mutations.test.js @@ -0,0 +1,103 @@ +import { describe, it, expect } from 'vitest'; + +import { SymmetricMatrix } from '../../index'; + +describe('Symmetric mutation', () => { + it('addSide', () => { + const matrix = new SymmetricMatrix([ + [0, 1, 2], + [1, 1, 0], + [2, 0, 2], + ]); + + expect(matrix.addSide).toBe(matrix.addRow); + expect(matrix.addSide).toBe(matrix.addColumn); + + matrix.addSide([4, 0, 0, 4]); + expect(matrix.to2DArray()).toStrictEqual([ + [0, 1, 2, 4], + [1, 1, 0, 0], + [2, 0, 2, 0], + [4, 0, 0, 4], + ]); + + matrix.addSide(2, [1.5, 1.5, 1.5, 1.5, 1.5]); + expect(matrix.to2DArray()).toStrictEqual([ + [0, 1, 1.5, 2, 4], + [1, 1, 1.5, 0, 0], + [1.5, 1.5, 1.5, 1.5, 1.5], + [2, 0, 1.5, 2, 0], + [4, 0, 1.5, 0, 4], + ]); + }); + + it('removeSide', () => { + const matrix = new SymmetricMatrix([ + [0, 1, 1.5, 2, 4], + [1, 1, 1.5, 0, 0], + [1.5, 1.5, 1.5, 1.5, 1.5], + [2, 0, 1.5, 2, 0], + [4, 0, 1.5, 0, 4], + ]); + + expect(matrix.removeSide).toBe(matrix.removeRow); + expect(matrix.removeSide).toBe(matrix.removeColumn); + + matrix.removeSide(2); + expect(matrix.to2DArray()).toStrictEqual([ + [0, 1, 2, 4], + [1, 1, 0, 0], + [2, 0, 2, 0], + [4, 0, 0, 4], + ]); + }); + + it('applyMask', () => { + let matrix = new SymmetricMatrix([ + [0, 1, 1.5, 2, 4], + [1, 1, 1.5, 0, 0], + [1.5, 1.5, 1.5, 1.5, 1.5], + [2, 0, 1.5, 2, 0], + [4, 0, 1.5, 0, 4], + ]); + + matrix.applyMask([1, 1, 0, 1, 0]); + expect(matrix.to2DArray()).toStrictEqual([ + [0, 1, 2], + [1, 1, 0], + [2, 0, 2], + ]); + + expect(() => matrix.applyMask([0, 1])).throw( + RangeError, + 'Mask size do not match with matrix size', + ); + expect(() => matrix.applyMask([0, 1, 1, 0])).throw( + RangeError, + 'Mask size do not match with matrix size', + ); + + matrix = new SymmetricMatrix([ + [0, 1, 1.5, 2, 4], + [1, 1, 1.5, 0, 0], + [1.5, 1.5, 1.5, 1.5, 1.5], + [2, 0, 1.5, 2, 0], + [4, 0, 1.5, 0, 4], + ]); + + // only falsy values are false and 0 + matrix.applyMask([true, false, 12, 0, -1]); + expect(matrix.to2DArray()).toStrictEqual([ + [0, 1.5, 4], + [1.5, 1.5, 1.5], + [4, 1.5, 4], + ]); + }); + + it('set', () => { + const matrix = SymmetricMatrix.zeros(3); + matrix.set(0, 2, 9); + + expect(matrix.get(2, 0)).toBe(9); + }); +}); diff --git a/src/matrix.js b/src/matrix.js index 99f4cb8..d5699ca 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -1771,10 +1771,20 @@ export class SymmetricMatrix extends Matrix { return Matrix.isMatrix(value) && value.klassType === 'SymmetricMatrix'; } - static zeros(rows, columns) { - return new this(rows, columns); + /** + * + * @param sideSize + * @return {SymmetricMatrix} + */ + static zeros(sideSize) { + return new this(sideSize); } + /** + * + * @param sideSize + * @return {SymmetricMatrix} + */ static ones(rows, columns) { return new this(rows, columns).fill(1); } @@ -1836,34 +1846,23 @@ export class SymmetricMatrix extends Matrix { } addSide(index, array) { - super.addRow(index, array); + if (array === undefined) { + array = index; + index = this.sideSize; + } + + super.addRow(index, array.slice(0, -1)); super.addColumn(index, array); return this; } - removeRow(index) { - return this.removeSide(index); - } - - addRow(index, array) { - return this.addSide(index, array); - } - - removeColumn(index) { - return this.removeSide(index); - } - - addColumn(index, array) { - return this.addSide(index, array); - } - /** * @param {Mask[]} mask */ applyMask(mask) { if (mask.length !== this.sideSize) { - throw new RangeError('mask size do not match with matrix size'); + throw new RangeError('Mask size do not match with matrix size'); } // prepare sides to remove from matrix from mask @@ -1918,6 +1917,7 @@ export class SymmetricMatrix extends Matrix { /** * @param {number[]} compact + * @return {SymmetricMatrix} */ static fromCompact(compact) { const compactSize = compact.length; @@ -1928,7 +1928,7 @@ export class SymmetricMatrix extends Matrix { if (!Number.isInteger(sideSize)) { throw new TypeError( - `this array is not a compact representation of a Symmetric, ${JSON.stringify( + `This array is not a compact representation of a Symmetric Matrix, ${JSON.stringify( compact, )}`, ); @@ -1936,7 +1936,7 @@ export class SymmetricMatrix extends Matrix { const matrix = new SymmetricMatrix(sideSize); for (let col = 0, row = 0, index = 0; index < compactSize; index++) { - matrix.set(col, row); + matrix.set(col, row, compact[index]); if (++col >= sideSize) col = ++row; } @@ -1944,6 +1944,12 @@ export class SymmetricMatrix extends Matrix { } } SymmetricMatrix.prototype.klassType = 'SymmetricMatrix'; +// eslint-disable-next-line no-multi-assign +SymmetricMatrix.prototype.removeRow = SymmetricMatrix.prototype.removeColumn = + SymmetricMatrix.prototype.removeSide; +// eslint-disable-next-line no-multi-assign +SymmetricMatrix.prototype.addRow = SymmetricMatrix.prototype.addColumn = + SymmetricMatrix.prototype.addSide; export class DistanceMatrix extends SymmetricMatrix { /** @@ -2032,7 +2038,7 @@ export class DistanceMatrix extends SymmetricMatrix { const matrix = new this(sideSize); for (let col = 1, row = 0, index = 0; index < compactSize; index++) { - matrix.set(col, row); + matrix.set(col, row, compact[index]); if (++col >= sideSize) col = ++row + 1; } From c98d7d85c3d79e6677aa2531be4a01813234d6b8 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Mon, 27 Nov 2023 14:26:00 +0100 Subject: [PATCH 09/20] test: test for distance matrix --- .../distance_matrix/creation.test.js | 101 ++++++++++++++++++ src/__tests__/distance_matrix/export.test.js | 15 +++ .../distance_matrix/mutations.test.js | 43 ++++++++ src/matrix.js | 21 +++- 4 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 src/__tests__/distance_matrix/creation.test.js create mode 100644 src/__tests__/distance_matrix/export.test.js create mode 100644 src/__tests__/distance_matrix/mutations.test.js diff --git a/src/__tests__/distance_matrix/creation.test.js b/src/__tests__/distance_matrix/creation.test.js new file mode 100644 index 0000000..45a8336 --- /dev/null +++ b/src/__tests__/distance_matrix/creation.test.js @@ -0,0 +1,101 @@ +import { describe, it, expect } from 'vitest'; + +import { Matrix, DistanceMatrix } from '../../index'; + +describe('DistanceMatrix creation', () => { + it('should create a new object', () => { + const payload = [ + [0, 2, 3], + [2, 0, 5], + [3, 5, 0], + ]; + const matrix = new DistanceMatrix(payload); + expect(matrix).not.toBe(payload); + expect(matrix).toBeInstanceOf(DistanceMatrix); + expect(DistanceMatrix.isDistanceMatrix(matrix)).toBe(true); + }); + + it('should throw with wrong arguments', () => { + expect(() => new DistanceMatrix(-1)).toThrow( + /^First argument must be a positive number or an array/, + ); + expect(() => new DistanceMatrix([0, 1, 2, 3])).toThrow( + /^Data must be a 2D array/, + ); + expect( + () => + new DistanceMatrix([ + [0, 1], + [0, 1, 2], + ]), + ).toThrow(/^Inconsistent array dimensions$/); + expect(() => new DistanceMatrix()).toThrow( + /^First argument must be a positive number or an array$/, + ); + expect( + () => + new DistanceMatrix([ + [1, 2, 3], + [4, 5, 6], + [7, undefined, 9], + ]), + ).toThrow(/^Input data contains non-numeric values$/); + expect( + () => + new DistanceMatrix([ + [0, 1, 2], + [2, 1, 3], + ]), + ).toThrow('not symmetric data'); + expect( + () => + new DistanceMatrix([ + [0, 1, 2], + [1, 1, 2], + [3, 2, 1], + ]), + ).toThrow('not symmetric data'); + expect( + () => + new DistanceMatrix( + new Matrix([ + [0, 1, 2], + [1, 0, 2], + [2, 2, 1], + ]), + ), + ).toThrow('Provided arguments do no produce a distance matrix'); + }); + + it('should create a distance matrix from compact 1D array', () => { + const matrix = DistanceMatrix.fromCompact([1, 2, 3, 4, 5, 6]); + expect(matrix.sideSize).toBe(4); + expect(matrix).toStrictEqual( + new DistanceMatrix([ + [0, 1, 2, 3], + [1, 0, 4, 5], + [2, 4, 0, 6], + [3, 5, 6, 0], + ]), + ); + expect(() => DistanceMatrix.fromCompact([0, 1, 2, 3, 4, 5, 6])).toThrow( + /^This array is not a compact representation of a DistanceMatrix/, + ); + }); + + it('zeros', () => { + expect(DistanceMatrix.zeros(3).to2DArray()).toStrictEqual([ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + ]); + }); + + it('ones', () => { + expect(DistanceMatrix.ones(3).to2DArray()).toStrictEqual([ + [0, 1, 1], + [1, 0, 1], + [1, 1, 0], + ]); + }); +}); diff --git a/src/__tests__/distance_matrix/export.test.js b/src/__tests__/distance_matrix/export.test.js new file mode 100644 index 0000000..1c5fa03 --- /dev/null +++ b/src/__tests__/distance_matrix/export.test.js @@ -0,0 +1,15 @@ +import { describe, it, expect } from 'vitest'; + +import { DistanceMatrix } from '../../index'; + +describe('DistanceMatrix export', () => { + it('toCompact', () => { + const matrix = new DistanceMatrix([ + [0, 1, 2, 3], + [1, 0, 2, 3], + [2, 2, 0, 3], + [3, 3, 3, 0], + ]); + expect(matrix.toCompact()).toStrictEqual([1, 2, 3, 2, 3, 3]); + }); +}); diff --git a/src/__tests__/distance_matrix/mutations.test.js b/src/__tests__/distance_matrix/mutations.test.js new file mode 100644 index 0000000..fecbd99 --- /dev/null +++ b/src/__tests__/distance_matrix/mutations.test.js @@ -0,0 +1,43 @@ +import { describe, it, expect } from 'vitest'; + +import { DistanceMatrix } from '../../index'; + +describe('DistanceMatrix mutation', () => { + it('addSide', () => { + const matrix = new DistanceMatrix([ + [0, 1, 2], + [1, 0, 3], + [2, 3, 0], + ]); + + expect(matrix.addSide).toBe(matrix.addRow); + expect(matrix.addSide).toBe(matrix.addColumn); + + matrix.addSide([3, 4, 5, 6]); + expect(matrix.to2DArray()).toStrictEqual([ + [0, 1, 2, 3], + [1, 0, 3, 4], + [2, 3, 0, 5], + [3, 4, 5, 0], + ]); + + matrix.addSide(2, [9, 9, 9, 9, 9]); + expect(matrix.to2DArray()).toStrictEqual([ + [0, 1, 9, 2, 3], + [1, 0, 9, 3, 4], + [9, 9, 0, 9, 9], + [2, 3, 9, 0, 5], + [3, 4, 9, 5, 0], + ]); + }); + + it('set', () => { + const matrix = DistanceMatrix.zeros(3); + + matrix.set(0, 2, 9); + expect(matrix.get(2, 0)).toBe(9); + + matrix.set(1, 1, 9); + expect(matrix.get(1, 1)).toBe(0); + }); +}); diff --git a/src/matrix.js b/src/matrix.js index d5699ca..0d62051 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -1785,8 +1785,8 @@ export class SymmetricMatrix extends Matrix { * @param sideSize * @return {SymmetricMatrix} */ - static ones(rows, columns) { - return new this(rows, columns).fill(1); + static ones(sideSize) { + return new this(sideSize).fill(1); } get sideSize() { @@ -1970,7 +1970,7 @@ export class DistanceMatrix extends SymmetricMatrix { super(sideSize); if (!this.isDistance()) { - throw new TypeError('provided arguments do no produce a distance matrix'); + throw new TypeError('Provided arguments do no produce a distance matrix'); } } @@ -1981,6 +1981,19 @@ export class DistanceMatrix extends SymmetricMatrix { return super.set(rowIndex, columnIndex, value); } + addSide(index, array) { + if (array === undefined) { + array = index; + index = this.sideSize; + } + + // ensure distance + // array = array.slice(); + // array[index] = 0; + + return super.addSide(index, array); + } + toSymmetricMatrix() { return new SymmetricMatrix(this); } @@ -2030,7 +2043,7 @@ export class DistanceMatrix extends SymmetricMatrix { if (!Number.isInteger(sideSize)) { throw new TypeError( - `this array is not a compact representation of a DistanceMatrix, ${JSON.stringify( + `This array is not a compact representation of a DistanceMatrix, ${JSON.stringify( compact, )}`, ); From 19e50276ce43d1bcc7123e1f85e2367e3f94cc1e Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:10:20 +0100 Subject: [PATCH 10/20] fix: some bugs --- src/matrix.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/matrix.js b/src/matrix.js index 0d62051..824893c 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -1645,7 +1645,7 @@ export default class Matrix extends AbstractMatrix { super(); if (Matrix.isMatrix(nRows)) { // eslint-disable-next-line no-constructor-return - return nRows.clone(); + return Matrix.prototype.clone.call(nRows); } else if (Number.isInteger(nRows) && nRows >= 0) { // Create an empty matrix this.data = []; @@ -1796,7 +1796,7 @@ export class SymmetricMatrix extends Matrix { constructor(sideSize) { if (SymmetricMatrix.isSymmetricMatrix(sideSize)) { // eslint-disable-next-line no-constructor-return - return sideSize.clone(); + return SymmetricMatrix.prototype.clone.call(sideSize); } if (typeof sideSize === 'number') { @@ -1851,7 +1851,10 @@ export class SymmetricMatrix extends Matrix { index = this.sideSize; } - super.addRow(index, array.slice(0, -1)); + const row = array.slice(); + row.splice(index, 1); + + super.addRow(index, row); super.addColumn(index, array); return this; @@ -1988,8 +1991,8 @@ export class DistanceMatrix extends SymmetricMatrix { } // ensure distance - // array = array.slice(); - // array[index] = 0; + array = array.slice(); + array[index] = 0; return super.addSide(index, array); } @@ -2023,10 +2026,10 @@ export class DistanceMatrix extends SymmetricMatrix { /** @type {number[]} */ const compact = new Array(compactLength); - for (let col = 0, row = 0, index = 0; index < compact.length; index++) { + for (let col = 1, row = 0, index = 0; index < compact.length; index++) { compact[index] = this.get(row, col); - if (++col >= this.sideSize) col = ++row; + if (++col >= this.sideSize) col = ++row + 1; } return compact; @@ -2059,3 +2062,5 @@ export class DistanceMatrix extends SymmetricMatrix { } } DistanceMatrix.prototype.klassSubType = 'DistanceMatrix'; +DistanceMatrix.prototype.addRow = DistanceMatrix.prototype.addSide; +DistanceMatrix.prototype.addColumn = DistanceMatrix.prototype.addSide; From a562252719b52e54e5985a6a9bd42566e0ce27ec Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:40:16 +0100 Subject: [PATCH 11/20] fix: complete coverage --- src/__tests__/distance_matrix/export.test.js | 15 ++++++++++++- src/matrix.js | 23 ++++++++++++-------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/__tests__/distance_matrix/export.test.js b/src/__tests__/distance_matrix/export.test.js index 1c5fa03..fbdf1ff 100644 --- a/src/__tests__/distance_matrix/export.test.js +++ b/src/__tests__/distance_matrix/export.test.js @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { DistanceMatrix } from '../../index'; +import { DistanceMatrix, SymmetricMatrix } from '../../index'; describe('DistanceMatrix export', () => { it('toCompact', () => { @@ -12,4 +12,17 @@ describe('DistanceMatrix export', () => { ]); expect(matrix.toCompact()).toStrictEqual([1, 2, 3, 2, 3, 3]); }); + + it('toSymmetricMatrix', () => { + const matrix = new DistanceMatrix([ + [0, 1, 2, 3], + [1, 0, 2, 3], + [2, 2, 0, 3], + [3, 3, 3, 0], + ]); + const sMatrix = matrix.toSymmetricMatrix(); + expect(matrix).not.toBe(sMatrix); + expect(sMatrix).not.toBeInstanceOf(DistanceMatrix); + expect(sMatrix).toBeInstanceOf(SymmetricMatrix); + }); }); diff --git a/src/matrix.js b/src/matrix.js index 824893c..5039f1f 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -1811,16 +1811,11 @@ export class SymmetricMatrix extends Matrix { } clone() { - /* - * Optimized matrix cloning support inheritance - * create with current prototype and add data - * skip constructor checks and full-scan iterations - */ - const matrix = Object.create(this.constructor.prototype); + const matrix = new SymmetricMatrix(this.sideSize); - // eslint-disable-next-line no-multi-assign - matrix.rows = matrix.columns = this.sideSize; - matrix.data = this.data.map((row) => row.slice()); + for (const [row, col, value] of this.upperRightEntries()) { + matrix.set(row, col, value); + } return matrix; } @@ -2001,6 +1996,16 @@ export class DistanceMatrix extends SymmetricMatrix { return new SymmetricMatrix(this); } + clone() { + const matrix = new DistanceMatrix(this.sideSize); + + for (const [row, col, value] of this.upperRightEntries()) { + matrix.set(row, col, value); + } + + return matrix; + } + /** * Compact format upper-right corner of matrix * no diagonal (only zeros) From 5b5c5ce2192755381a02514578ffb38ad8b4e2b4 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:49:18 +0100 Subject: [PATCH 12/20] fix: complete coverage --- src/__tests__/distance_matrix/creation.test.js | 6 ++++++ src/__tests__/matrix/creation.test.js | 11 ++++++++++- src/__tests__/symmetric_matrix/creation.test.js | 9 ++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/__tests__/distance_matrix/creation.test.js b/src/__tests__/distance_matrix/creation.test.js index 45a8336..b52dff6 100644 --- a/src/__tests__/distance_matrix/creation.test.js +++ b/src/__tests__/distance_matrix/creation.test.js @@ -98,4 +98,10 @@ describe('DistanceMatrix creation', () => { [1, 1, 0], ]); }); + + it('clone', () => { + const matrix = DistanceMatrix.zeros(3); + const clone = matrix.clone(); + expect(clone).toBeInstanceOf(DistanceMatrix); + }); }); diff --git a/src/__tests__/matrix/creation.test.js b/src/__tests__/matrix/creation.test.js index 70d96f0..caee67f 100644 --- a/src/__tests__/matrix/creation.test.js +++ b/src/__tests__/matrix/creation.test.js @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { Matrix, wrap } from '../..'; +import { DistanceMatrix, Matrix, SymmetricMatrix, wrap } from '../..'; import * as util from '../../../testUtils'; import MatrixTransposeView from '../../views/transpose'; @@ -243,4 +243,13 @@ describe('Matrix creation', () => { ); expect(JSON.stringify(transposeTwice)).toBe(json); }); + + it('construct from child class', () => { + const dMatrix = DistanceMatrix.zeros(3); + const sMatrix = new SymmetricMatrix(dMatrix); + const matrix = new Matrix(sMatrix); + expect(matrix).not.toBeInstanceOf(DistanceMatrix); + expect(matrix).not.toBeInstanceOf(SymmetricMatrix); + expect(matrix).toBeInstanceOf(Matrix); + }); }); diff --git a/src/__tests__/symmetric_matrix/creation.test.js b/src/__tests__/symmetric_matrix/creation.test.js index 06966e2..52554ac 100644 --- a/src/__tests__/symmetric_matrix/creation.test.js +++ b/src/__tests__/symmetric_matrix/creation.test.js @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { Matrix, SymmetricMatrix } from '../../index'; +import { DistanceMatrix, Matrix, SymmetricMatrix } from '../../index'; describe('SymmetricMatrix creation', () => { it('should create a new object', () => { @@ -159,4 +159,11 @@ describe('SymmetricMatrix creation', () => { expect(matrix).not.toBeInstanceOf(SymmetricMatrix); expect(matrix).toBeInstanceOf(Matrix); }); + + it('construct from child class', () => { + const dMatrix = DistanceMatrix.zeros(3); + const sMatrix = new SymmetricMatrix(dMatrix); + expect(sMatrix).not.toBeInstanceOf(DistanceMatrix); + expect(sMatrix).toBeInstanceOf(SymmetricMatrix); + }); }); From a7e17891f80eafbf2a8f95194412275d366bd986 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Tue, 28 Nov 2023 17:33:45 +0100 Subject: [PATCH 13/20] wip: refactor inherit from AbstractMatrix implementation with Matrix composition --- matrix.d.ts | 61 +++++++----------- src/matrix.js | 170 +++++++++++++++++++++++++++++++------------------- 2 files changed, 127 insertions(+), 104 deletions(-) diff --git a/matrix.d.ts b/matrix.d.ts index a172fe1..9132767 100644 --- a/matrix.d.ts +++ b/matrix.d.ts @@ -734,7 +734,9 @@ export abstract class AbstractMatrix { /** * Creates an exact and independent copy of the matrix. */ - clone(): Matrix; + clone(): this; + + static copy(from: AbstractMatrix, to: M): M; /** * Returns the sum of all elements of the matrix. @@ -1037,36 +1039,36 @@ export class SymmetricMatrix extends Matrix { /** * Creates a symmetric matrix with the given dimensions. Values will be set to zero. * This is equivalent to calling the Matrix constructor. - * @param sidesSize - Number of rows or columns (square). + * @param diagonalSize - Number of rows or columns (square). * @returns The new symmetric matrix. */ - static zeros(sidesSize: number): SymmetricMatrix; + static zeros(diagonalSize: number): SymmetricMatrix; /** * Creates a symmetric matrix with the given dimensions. Values will be set to one. - * @param sidesSize - Number of rows or columns (square). + * @param diagonalSize - Number of rows or columns (square). * @returns The new symmetric matrix. */ - static ones(sidesSize: number): SymmetricMatrix; + static ones(diagonalSize: number): SymmetricMatrix; static isSymmetricMatrix(value: unknown): value is SymmetricMatrix; /** * alias for `rows` or `columns` (square matrix so equals) */ - get sideSize(): number; + get diagonalSize(): number; - constructor(sidesSize: number); - /** - * @throws TypeError if data are not symmetric - * @param data - */ - constructor(data: ArrayLike>); /** * @throws TypeError if otherMatrix is not symmetric * @param otherMatrix */ constructor(otherMatrix: AbstractMatrix); + constructor(diagonalSize: number); + /** + * @throws TypeError if data are not symmetric + * @param data + */ + constructor(data: ArrayLike>); /** * copy to a new matrix @@ -1077,40 +1079,15 @@ export class SymmetricMatrix extends Matrix { * Symmetric remove row / column * @param index */ - removeSide(index: number): this; + removeCross(index: number): this; /** * Symmetric add row / column * @param index * @param array */ - addSide(index: number, array: ArrayLike | AbstractMatrix): this; - addSide(array: ArrayLike | AbstractMatrix): this; - - /** - * alias to removeSide - * @param index - */ - removeRow(index: number): this; - /** - * alias to addSide - * @param index - * @param array - */ - addRow(index: number, array: ArrayLike | AbstractMatrix): this; - addRow(array: ArrayLike | AbstractMatrix): this; - /** - * alias to removeSide - * @param index - */ - removeColumn(index: number): this; - /** - * alias to addSide - * @param index - * @param array - */ - addColumn(index: number, array: ArrayLike | AbstractMatrix): this; - addColumn(array: ArrayLike | AbstractMatrix): this; + addCross(index: number, array: ArrayLike | AbstractMatrix): this; + addCross(array: ArrayLike | AbstractMatrix): this; /** * remove sides (rows / columns) with falsy value from mask. @@ -1163,6 +1140,8 @@ export class SymmetricMatrix extends Matrix { * @param compact */ static fromCompact(compact: number[]): SymmetricMatrix; + + clone(): this; } export class DistanceMatrix extends SymmetricMatrix { @@ -1235,6 +1214,8 @@ export class DistanceMatrix extends SymmetricMatrix { * @param compact */ static fromCompact(compact: number[]): DistanceMatrix; + + clone(): this; } export class MatrixColumnView extends AbstractMatrix { diff --git a/src/matrix.js b/src/matrix.js index 5039f1f..26baeec 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -1324,13 +1324,21 @@ export class AbstractMatrix { } clone() { - let newMatrix = new Matrix(this.rows, this.columns); - for (let row = 0; row < this.rows; row++) { - for (let column = 0; column < this.columns; column++) { - newMatrix.set(row, column, this.get(row, column)); - } + return this.constructor.copy(this, new Matrix(this.rows, this.columns)); + } + + /** + * @template {AbstractMatrix} M + * @param {AbstractMatrix} from + * @param {M} to + * @return {M} + */ + static copy(from, to) { + for (const [row, column, value] of from.entries()) { + to.set(row, column, value); } - return newMatrix; + + return to; } sum(by) { @@ -1641,21 +1649,33 @@ export default class Matrix extends AbstractMatrix { */ data; + /** + * Init an empty matrix + * @param {number} nRows + * @param {number} nColumns + */ + #initData(nRows, nColumns) { + this.data = []; + + if (Number.isInteger(nColumns) && nColumns >= 0) { + for (let i = 0; i < nRows; i++) { + this.data.push(new Float64Array(nColumns)); + } + } else { + throw new TypeError('nColumns must be a positive integer'); + } + + this.rows = nRows; + this.columns = nColumns; + } + constructor(nRows, nColumns) { super(); if (Matrix.isMatrix(nRows)) { - // eslint-disable-next-line no-constructor-return - return Matrix.prototype.clone.call(nRows); + this.#initData(nRows.rows, nRows.columns); + Matrix.copy(nRows, this); } else if (Number.isInteger(nRows) && nRows >= 0) { - // Create an empty matrix - this.data = []; - if (Number.isInteger(nColumns) && nColumns >= 0) { - for (let i = 0; i < nRows; i++) { - this.data.push(new Float64Array(nColumns)); - } - } else { - throw new TypeError('nColumns must be a positive integer'); - } + this.#initData(nRows, nColumns); } else if (isAnyArray(nRows)) { // Copy the values from the 2D array const arrayData = nRows; @@ -1667,6 +1687,7 @@ export default class Matrix extends AbstractMatrix { ); } this.data = []; + for (let i = 0; i < nRows; i++) { if (arrayData[i].length !== nColumns) { throw new RangeError('Inconsistent array dimensions'); @@ -1676,13 +1697,14 @@ export default class Matrix extends AbstractMatrix { } this.data.push(Float64Array.from(arrayData[i])); } + + this.rows = nRows; + this.columns = nColumns; } else { throw new TypeError( 'First argument must be a positive number or an array', ); } - this.rows = nRows; - this.columns = nColumns; } set(rowIndex, columnIndex, value) { @@ -1759,7 +1781,26 @@ installMathOperations(AbstractMatrix, Matrix); * @typedef {0 | 1 | number | boolean} Mask */ -export class SymmetricMatrix extends Matrix { +export class SymmetricMatrix extends AbstractMatrix { + /** @type {Matrix} */ + #matrix; + + get size() { + return this.#matrix.size; + } + + get rows() { + return this.#matrix.rows; + } + + get columns() { + return this.#matrix.columns; + } + + get diagonalSize() { + return this.rows; + } + /** * not the same as matrix.isSymmetric() * Here is to check if it's instanceof SymmetricMatrix without bundling issues @@ -1772,37 +1813,41 @@ export class SymmetricMatrix extends Matrix { } /** - * - * @param sideSize + * @param diagonalSize * @return {SymmetricMatrix} */ - static zeros(sideSize) { - return new this(sideSize); + static zeros(diagonalSize) { + return new this(diagonalSize); } /** - * - * @param sideSize + * @param diagonalSize * @return {SymmetricMatrix} */ - static ones(sideSize) { - return new this(sideSize).fill(1); + static ones(diagonalSize) { + return new this(diagonalSize).fill(1); } - get sideSize() { - return this.rows; - } + /** + * @param {number | AbstractMatrix | ArrayBuffer>} diagonalSize + * @return {this} + */ + constructor(diagonalSize) { + super(); - constructor(sideSize) { - if (SymmetricMatrix.isSymmetricMatrix(sideSize)) { - // eslint-disable-next-line no-constructor-return - return SymmetricMatrix.prototype.clone.call(sideSize); - } + if (Matrix.isMatrix(diagonalSize)) { + if (!diagonalSize.isSymmetric()) { + throw new TypeError('not symmetric data'); + } - if (typeof sideSize === 'number') { - super(sideSize, sideSize); + this.#matrix = Matrix.copy( + diagonalSize, + new Matrix(diagonalSize.rows, diagonalSize.rows), + ); + } else if (Number.isInteger(diagonalSize) && diagonalSize >= 0) { + this.#matrix = new Matrix(diagonalSize, diagonalSize); } else { - super(sideSize); + this.#matrix = new Matrix(diagonalSize); if (!this.isSymmetric()) { throw new TypeError('not symmetric data'); @@ -1811,7 +1856,7 @@ export class SymmetricMatrix extends Matrix { } clone() { - const matrix = new SymmetricMatrix(this.sideSize); + const matrix = new SymmetricMatrix(this.diagonalSize); for (const [row, col, value] of this.upperRightEntries()) { matrix.set(row, col, value); @@ -1824,33 +1869,36 @@ export class SymmetricMatrix extends Matrix { return new Matrix(this); } + get(rowIndex, columnIndex) { + return this.#matrix.get(rowIndex, columnIndex); + } set(rowIndex, columnIndex, value) { // symmetric set - super.set(rowIndex, columnIndex, value); - super.set(columnIndex, rowIndex, value); + this.#matrix.set(rowIndex, columnIndex, value); + this.#matrix.set(columnIndex, rowIndex, value); return this; } - removeSide(index) { + removeCross(index) { // symmetric remove side - super.removeRow(index); - super.removeColumn(index); + this.#matrix.removeRow(index); + this.#matrix.removeColumn(index); return this; } - addSide(index, array) { + addCross(index, array) { if (array === undefined) { array = index; - index = this.sideSize; + index = this.diagonalSize; } const row = array.slice(); row.splice(index, 1); - super.addRow(index, row); - super.addColumn(index, array); + this.#matrix.addRow(index, row); + this.#matrix.addColumn(index, array); return this; } @@ -1859,7 +1907,7 @@ export class SymmetricMatrix extends Matrix { * @param {Mask[]} mask */ applyMask(mask) { - if (mask.length !== this.sideSize) { + if (mask.length !== this.diagonalSize) { throw new RangeError('Mask size do not match with matrix size'); } @@ -1875,7 +1923,7 @@ export class SymmetricMatrix extends Matrix { // remove sides for (const sideIndex of sidesToRemove) { - this.removeSide(sideIndex); + this.removeCross(sideIndex); } return this; @@ -1900,14 +1948,14 @@ export class SymmetricMatrix extends Matrix { * @returns {number[]} */ toCompact() { - const { sideSize } = this; + const { diagonalSize } = this; /** @type {number[]} */ - const compact = new Array((sideSize * (sideSize + 1)) / 2); + const compact = new Array((diagonalSize * (diagonalSize + 1)) / 2); for (let col = 0, row = 0, index = 0; index < compact.length; index++) { compact[index] = this.get(row, col); - if (++col >= this.sideSize) col = ++row; + if (++col >= diagonalSize) col = ++row; } return compact; @@ -1922,9 +1970,9 @@ export class SymmetricMatrix extends Matrix { // compactSize = (sideSize * (sideSize + 1)) / 2 // https://mathsolver.microsoft.com/fr/solve-problem/y%20%3D%20%20x%20%60cdot%20%20%20%60frac%7B%20%20%60left(%20x%2B1%20%20%60right)%20%20%20%20%7D%7B%202%20%20%7D // sideSize = (Sqrt(8 × compactSize + 1) - 1) / 2 - const sideSize = (Math.sqrt(8 * compactSize + 1) - 1) / 2; + const diagonalSize = (Math.sqrt(8 * compactSize + 1) - 1) / 2; - if (!Number.isInteger(sideSize)) { + if (!Number.isInteger(diagonalSize)) { throw new TypeError( `This array is not a compact representation of a Symmetric Matrix, ${JSON.stringify( compact, @@ -1932,22 +1980,16 @@ export class SymmetricMatrix extends Matrix { ); } - const matrix = new SymmetricMatrix(sideSize); + const matrix = new SymmetricMatrix(diagonalSize); for (let col = 0, row = 0, index = 0; index < compactSize; index++) { matrix.set(col, row, compact[index]); - if (++col >= sideSize) col = ++row; + if (++col >= diagonalSize) col = ++row; } return matrix; } } SymmetricMatrix.prototype.klassType = 'SymmetricMatrix'; -// eslint-disable-next-line no-multi-assign -SymmetricMatrix.prototype.removeRow = SymmetricMatrix.prototype.removeColumn = - SymmetricMatrix.prototype.removeSide; -// eslint-disable-next-line no-multi-assign -SymmetricMatrix.prototype.addRow = SymmetricMatrix.prototype.addColumn = - SymmetricMatrix.prototype.addSide; export class DistanceMatrix extends SymmetricMatrix { /** From 02a318778e31d84cd23f77b9b8088e884607235f Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Tue, 28 Nov 2023 17:37:50 +0100 Subject: [PATCH 14/20] wip: refactor inherit from AbstractMatrix implementation with Matrix composition --- src/matrix.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/matrix.js b/src/matrix.js index 26baeec..2b4a2ef 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -1,4 +1,5 @@ import { isAnyArray } from 'is-any-array'; +import { index } from 'mathjs'; import rescale from 'ml-array-rescale'; import { inspectMatrix, inspectMatrixWithOptions } from './inspect'; @@ -2021,17 +2022,17 @@ export class DistanceMatrix extends SymmetricMatrix { return super.set(rowIndex, columnIndex, value); } - addSide(index, array) { + addCross(index, array) { if (array === undefined) { array = index; - index = this.sideSize; + index = this.diagonalSize; } // ensure distance array = array.slice(); array[index] = 0; - return super.addSide(index, array); + return super.addCross(index, array); } toSymmetricMatrix() { @@ -2039,9 +2040,10 @@ export class DistanceMatrix extends SymmetricMatrix { } clone() { - const matrix = new DistanceMatrix(this.sideSize); + const matrix = new DistanceMatrix(this.diagonalSize); for (const [row, col, value] of this.upperRightEntries()) { + if (row === col) continue; matrix.set(row, col, value); } @@ -2068,15 +2070,15 @@ export class DistanceMatrix extends SymmetricMatrix { * @returns {number[]} */ toCompact() { - const { sideSize } = this; - const compactLength = ((sideSize - 1) * sideSize) / 2; + const { diagonalSize } = this; + const compactLength = ((diagonalSize - 1) * diagonalSize) / 2; /** @type {number[]} */ const compact = new Array(compactLength); for (let col = 1, row = 0, index = 0; index < compact.length; index++) { compact[index] = this.get(row, col); - if (++col >= this.sideSize) col = ++row + 1; + if (++col >= diagonalSize) col = ++row + 1; } return compact; @@ -2089,9 +2091,9 @@ export class DistanceMatrix extends SymmetricMatrix { const compactSize = compact.length; // compactSize = (sideSize * (sideSize - 1)) / 2 // sideSize = (Sqrt(8 × compactSize + 1) + 1) / 2 - const sideSize = (Math.sqrt(8 * compactSize + 1) + 1) / 2; + const diagonalSize = (Math.sqrt(8 * compactSize + 1) + 1) / 2; - if (!Number.isInteger(sideSize)) { + if (!Number.isInteger(diagonalSize)) { throw new TypeError( `This array is not a compact representation of a DistanceMatrix, ${JSON.stringify( compact, @@ -2099,15 +2101,13 @@ export class DistanceMatrix extends SymmetricMatrix { ); } - const matrix = new this(sideSize); + const matrix = new this(diagonalSize); for (let col = 1, row = 0, index = 0; index < compactSize; index++) { matrix.set(col, row, compact[index]); - if (++col >= sideSize) col = ++row + 1; + if (++col >= diagonalSize) col = ++row + 1; } return matrix; } } DistanceMatrix.prototype.klassSubType = 'DistanceMatrix'; -DistanceMatrix.prototype.addRow = DistanceMatrix.prototype.addSide; -DistanceMatrix.prototype.addColumn = DistanceMatrix.prototype.addSide; From 173f34c503812b9d298765180d8cf3a2cd205d40 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Tue, 28 Nov 2023 17:46:37 +0100 Subject: [PATCH 15/20] refactor: extends AbstractMatrix impl by composition SymmetricMatrix implement AbstractMatrix using composition pattern with Matrix. DistanceMatrix keep extends SymmetricMatrix --- src/__tests__/distance_matrix/creation.test.js | 2 +- src/__tests__/distance_matrix/mutations.test.js | 7 ++----- src/__tests__/symmetric_matrix/creation.test.js | 4 ++-- src/__tests__/symmetric_matrix/mutations.test.js | 12 +++--------- src/matrix.js | 2 +- 5 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/__tests__/distance_matrix/creation.test.js b/src/__tests__/distance_matrix/creation.test.js index b52dff6..1b59287 100644 --- a/src/__tests__/distance_matrix/creation.test.js +++ b/src/__tests__/distance_matrix/creation.test.js @@ -69,7 +69,7 @@ describe('DistanceMatrix creation', () => { it('should create a distance matrix from compact 1D array', () => { const matrix = DistanceMatrix.fromCompact([1, 2, 3, 4, 5, 6]); - expect(matrix.sideSize).toBe(4); + expect(matrix.diagonalSize).toBe(4); expect(matrix).toStrictEqual( new DistanceMatrix([ [0, 1, 2, 3], diff --git a/src/__tests__/distance_matrix/mutations.test.js b/src/__tests__/distance_matrix/mutations.test.js index fecbd99..04d59f1 100644 --- a/src/__tests__/distance_matrix/mutations.test.js +++ b/src/__tests__/distance_matrix/mutations.test.js @@ -10,10 +10,7 @@ describe('DistanceMatrix mutation', () => { [2, 3, 0], ]); - expect(matrix.addSide).toBe(matrix.addRow); - expect(matrix.addSide).toBe(matrix.addColumn); - - matrix.addSide([3, 4, 5, 6]); + matrix.addCross([3, 4, 5, 6]); expect(matrix.to2DArray()).toStrictEqual([ [0, 1, 2, 3], [1, 0, 3, 4], @@ -21,7 +18,7 @@ describe('DistanceMatrix mutation', () => { [3, 4, 5, 0], ]); - matrix.addSide(2, [9, 9, 9, 9, 9]); + matrix.addCross(2, [9, 9, 9, 9, 9]); expect(matrix.to2DArray()).toStrictEqual([ [0, 1, 9, 2, 3], [1, 0, 9, 3, 4], diff --git a/src/__tests__/symmetric_matrix/creation.test.js b/src/__tests__/symmetric_matrix/creation.test.js index 52554ac..0ff4588 100644 --- a/src/__tests__/symmetric_matrix/creation.test.js +++ b/src/__tests__/symmetric_matrix/creation.test.js @@ -115,13 +115,13 @@ describe('SymmetricMatrix creation', () => { ]); expect(matrix.rows).toBe(3); expect(matrix.columns).toBe(3); - expect(matrix.sideSize).toBe(3); + expect(matrix.diagonalSize).toBe(3); expect(matrix.get(1, 2)).toBe(7); }); it('should create a symmetric matrix from compact 1D array', () => { const matrix = SymmetricMatrix.fromCompact([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); - expect(matrix.sideSize).toBe(4); + expect(matrix.diagonalSize).toBe(4); expect(matrix).toStrictEqual( new SymmetricMatrix([ [0, 1, 2, 3], diff --git a/src/__tests__/symmetric_matrix/mutations.test.js b/src/__tests__/symmetric_matrix/mutations.test.js index b3bbdb3..1558c4f 100644 --- a/src/__tests__/symmetric_matrix/mutations.test.js +++ b/src/__tests__/symmetric_matrix/mutations.test.js @@ -10,10 +10,7 @@ describe('Symmetric mutation', () => { [2, 0, 2], ]); - expect(matrix.addSide).toBe(matrix.addRow); - expect(matrix.addSide).toBe(matrix.addColumn); - - matrix.addSide([4, 0, 0, 4]); + matrix.addCross([4, 0, 0, 4]); expect(matrix.to2DArray()).toStrictEqual([ [0, 1, 2, 4], [1, 1, 0, 0], @@ -21,7 +18,7 @@ describe('Symmetric mutation', () => { [4, 0, 0, 4], ]); - matrix.addSide(2, [1.5, 1.5, 1.5, 1.5, 1.5]); + matrix.addCross(2, [1.5, 1.5, 1.5, 1.5, 1.5]); expect(matrix.to2DArray()).toStrictEqual([ [0, 1, 1.5, 2, 4], [1, 1, 1.5, 0, 0], @@ -40,10 +37,7 @@ describe('Symmetric mutation', () => { [4, 0, 1.5, 0, 4], ]); - expect(matrix.removeSide).toBe(matrix.removeRow); - expect(matrix.removeSide).toBe(matrix.removeColumn); - - matrix.removeSide(2); + matrix.removeCross(2); expect(matrix.to2DArray()).toStrictEqual([ [0, 1, 2, 4], [1, 1, 0, 0], diff --git a/src/matrix.js b/src/matrix.js index 2b4a2ef..416296a 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -1830,7 +1830,7 @@ export class SymmetricMatrix extends AbstractMatrix { } /** - * @param {number | AbstractMatrix | ArrayBuffer>} diagonalSize + * @param {number | AbstractMatrix | ArrayLike>} diagonalSize * @return {this} */ constructor(diagonalSize) { From 0d16a6bfa3f5c894832cb697421b73b006159182 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:47:25 +0100 Subject: [PATCH 16/20] test: coverage of .clone() --- .../symmetric_matrix/creation.test.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/__tests__/symmetric_matrix/creation.test.js b/src/__tests__/symmetric_matrix/creation.test.js index 0ff4588..d4127d8 100644 --- a/src/__tests__/symmetric_matrix/creation.test.js +++ b/src/__tests__/symmetric_matrix/creation.test.js @@ -29,7 +29,7 @@ describe('SymmetricMatrix creation', () => { ]); }); - it('should clone existing symmetric matrix', () => { + it('should clone existing symmetric matrix (from constructor)', () => { const original = new SymmetricMatrix([ [1, 2, 3], [2, 4, 5], @@ -42,6 +42,23 @@ describe('SymmetricMatrix creation', () => { expect(matrix).toStrictEqual(original); }); + it('should clone existing symmetric matrix (.clone())', () => { + const original = new SymmetricMatrix([ + [1, 2, 3], + [2, 4, 5], + [3, 5, 6], + ]); + const matrix = original.clone(); + expect(matrix).not.toBe(original); + expect(matrix).toBeInstanceOf(SymmetricMatrix); + expect(SymmetricMatrix.isSymmetricMatrix(matrix)).toBe(true); + expect(matrix).toStrictEqual(original); + + original.set(1, 1, 3); + expect(original.get(1, 1)).toBe(3); + expect(matrix.get(1, 1)).toBe(4); + }); + it('should create a zero symmetric matrix', () => { const matrix = new SymmetricMatrix(9); expect(matrix.rows).toBe(9); From bf05f94ef10c033c89023e69994f19d385ba94ce Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:55:53 +0100 Subject: [PATCH 17/20] refactor: cut into specific files --- src/distanceMatrix.js | 121 +++++++++++++++ src/index.js | 10 +- src/matrix.js | 335 ----------------------------------------- src/symmetricMatrix.js | 214 ++++++++++++++++++++++++++ 4 files changed, 338 insertions(+), 342 deletions(-) create mode 100644 src/distanceMatrix.js create mode 100644 src/symmetricMatrix.js diff --git a/src/distanceMatrix.js b/src/distanceMatrix.js new file mode 100644 index 0000000..75c620b --- /dev/null +++ b/src/distanceMatrix.js @@ -0,0 +1,121 @@ +import { SymmetricMatrix } from './symmetricMatrix'; + +export class DistanceMatrix extends SymmetricMatrix { + /** + * not the same as matrix.isSymmetric() + * Here is to check if it's instanceof SymmetricMatrix without bundling issues + * + * @param value + * @returns {boolean} + */ + static isDistanceMatrix(value) { + return ( + SymmetricMatrix.isSymmetricMatrix(value) && + value.klassSubType === 'DistanceMatrix' + ); + } + + constructor(sideSize) { + super(sideSize); + + if (!this.isDistance()) { + throw new TypeError('Provided arguments do no produce a distance matrix'); + } + } + + set(rowIndex, columnIndex, value) { + // distance matrix diagonal is 0 + if (rowIndex === columnIndex) value = 0; + + return super.set(rowIndex, columnIndex, value); + } + + addCross(index, array) { + if (array === undefined) { + array = index; + index = this.diagonalSize; + } + + // ensure distance + array = array.slice(); + array[index] = 0; + + return super.addCross(index, array); + } + + toSymmetricMatrix() { + return new SymmetricMatrix(this); + } + + clone() { + const matrix = new DistanceMatrix(this.diagonalSize); + + for (const [row, col, value] of this.upperRightEntries()) { + if (row === col) continue; + matrix.set(row, col, value); + } + + return matrix; + } + + /** + * Compact format upper-right corner of matrix + * no diagonal (only zeros) + * iterable from left to right, from top to bottom. + * + * ``` + * A B C D + * A 0 1 2 3 + * B 1 0 4 5 + * C 2 4 0 6 + * D 3 5 6 0 + * ``` + * + * will return compact 1D array `[1, 2, 3, 4, 5, 6]` + * + * length is S(i=0, n=sideSize-1) => 6 for a 4 side sized matrix + * + * @returns {number[]} + */ + toCompact() { + const { diagonalSize } = this; + const compactLength = ((diagonalSize - 1) * diagonalSize) / 2; + + /** @type {number[]} */ + const compact = new Array(compactLength); + for (let col = 1, row = 0, index = 0; index < compact.length; index++) { + compact[index] = this.get(row, col); + + if (++col >= diagonalSize) col = ++row + 1; + } + + return compact; + } + + /** + * @param {number[]} compact + */ + static fromCompact(compact) { + const compactSize = compact.length; + // compactSize = (sideSize * (sideSize - 1)) / 2 + // sideSize = (Sqrt(8 × compactSize + 1) + 1) / 2 + const diagonalSize = (Math.sqrt(8 * compactSize + 1) + 1) / 2; + + if (!Number.isInteger(diagonalSize)) { + throw new TypeError( + `This array is not a compact representation of a DistanceMatrix, ${JSON.stringify( + compact, + )}`, + ); + } + + const matrix = new this(diagonalSize); + for (let col = 1, row = 0, index = 0; index < compactSize; index++) { + matrix.set(col, row, compact[index]); + if (++col >= diagonalSize) col = ++row + 1; + } + + return matrix; + } +} +DistanceMatrix.prototype.klassSubType = 'DistanceMatrix'; diff --git a/src/index.js b/src/index.js index 3d19797..42ca1c9 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,6 @@ -export { - AbstractMatrix, - default, - default as Matrix, - SymmetricMatrix, - DistanceMatrix, -} from './matrix'; +export { AbstractMatrix, default, default as Matrix } from './matrix'; +export * from './symmetricMatrix'; +export * from './distanceMatrix'; export * from './views/index'; export { wrap } from './wrap/wrap'; diff --git a/src/matrix.js b/src/matrix.js index 416296a..68f6db6 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -1,5 +1,4 @@ import { isAnyArray } from 'is-any-array'; -import { index } from 'mathjs'; import rescale from 'ml-array-rescale'; import { inspectMatrix, inspectMatrixWithOptions } from './inspect'; @@ -1777,337 +1776,3 @@ export default class Matrix extends AbstractMatrix { } installMathOperations(AbstractMatrix, Matrix); - -/** - * @typedef {0 | 1 | number | boolean} Mask - */ - -export class SymmetricMatrix extends AbstractMatrix { - /** @type {Matrix} */ - #matrix; - - get size() { - return this.#matrix.size; - } - - get rows() { - return this.#matrix.rows; - } - - get columns() { - return this.#matrix.columns; - } - - get diagonalSize() { - return this.rows; - } - - /** - * not the same as matrix.isSymmetric() - * Here is to check if it's instanceof SymmetricMatrix without bundling issues - * - * @param value - * @returns {boolean} - */ - static isSymmetricMatrix(value) { - return Matrix.isMatrix(value) && value.klassType === 'SymmetricMatrix'; - } - - /** - * @param diagonalSize - * @return {SymmetricMatrix} - */ - static zeros(diagonalSize) { - return new this(diagonalSize); - } - - /** - * @param diagonalSize - * @return {SymmetricMatrix} - */ - static ones(diagonalSize) { - return new this(diagonalSize).fill(1); - } - - /** - * @param {number | AbstractMatrix | ArrayLike>} diagonalSize - * @return {this} - */ - constructor(diagonalSize) { - super(); - - if (Matrix.isMatrix(diagonalSize)) { - if (!diagonalSize.isSymmetric()) { - throw new TypeError('not symmetric data'); - } - - this.#matrix = Matrix.copy( - diagonalSize, - new Matrix(diagonalSize.rows, diagonalSize.rows), - ); - } else if (Number.isInteger(diagonalSize) && diagonalSize >= 0) { - this.#matrix = new Matrix(diagonalSize, diagonalSize); - } else { - this.#matrix = new Matrix(diagonalSize); - - if (!this.isSymmetric()) { - throw new TypeError('not symmetric data'); - } - } - } - - clone() { - const matrix = new SymmetricMatrix(this.diagonalSize); - - for (const [row, col, value] of this.upperRightEntries()) { - matrix.set(row, col, value); - } - - return matrix; - } - - toMatrix() { - return new Matrix(this); - } - - get(rowIndex, columnIndex) { - return this.#matrix.get(rowIndex, columnIndex); - } - set(rowIndex, columnIndex, value) { - // symmetric set - this.#matrix.set(rowIndex, columnIndex, value); - this.#matrix.set(columnIndex, rowIndex, value); - - return this; - } - - removeCross(index) { - // symmetric remove side - this.#matrix.removeRow(index); - this.#matrix.removeColumn(index); - - return this; - } - - addCross(index, array) { - if (array === undefined) { - array = index; - index = this.diagonalSize; - } - - const row = array.slice(); - row.splice(index, 1); - - this.#matrix.addRow(index, row); - this.#matrix.addColumn(index, array); - - return this; - } - - /** - * @param {Mask[]} mask - */ - applyMask(mask) { - if (mask.length !== this.diagonalSize) { - throw new RangeError('Mask size do not match with matrix size'); - } - - // prepare sides to remove from matrix from mask - /** @type {number[]} */ - const sidesToRemove = []; - for (const [index, passthroughs] of mask.entries()) { - if (passthroughs) continue; - sidesToRemove.push(index); - } - // to remove from highest to lowest for no mutation shifting - sidesToRemove.reverse(); - - // remove sides - for (const sideIndex of sidesToRemove) { - this.removeCross(sideIndex); - } - - return this; - } - - /** - * Compact format upper-right corner of matrix - * iterate from left to right, from top to bottom. - * - * ``` - * A B C D - * A 1 2 3 4 - * B 2 5 6 7 - * C 3 6 8 9 - * D 4 7 9 10 - * ``` - * - * will return compact 1D array `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]` - * - * length is S(i=0, n=sideSize) => 10 for a 4 sideSized matrix - * - * @returns {number[]} - */ - toCompact() { - const { diagonalSize } = this; - - /** @type {number[]} */ - const compact = new Array((diagonalSize * (diagonalSize + 1)) / 2); - for (let col = 0, row = 0, index = 0; index < compact.length; index++) { - compact[index] = this.get(row, col); - - if (++col >= diagonalSize) col = ++row; - } - - return compact; - } - - /** - * @param {number[]} compact - * @return {SymmetricMatrix} - */ - static fromCompact(compact) { - const compactSize = compact.length; - // compactSize = (sideSize * (sideSize + 1)) / 2 - // https://mathsolver.microsoft.com/fr/solve-problem/y%20%3D%20%20x%20%60cdot%20%20%20%60frac%7B%20%20%60left(%20x%2B1%20%20%60right)%20%20%20%20%7D%7B%202%20%20%7D - // sideSize = (Sqrt(8 × compactSize + 1) - 1) / 2 - const diagonalSize = (Math.sqrt(8 * compactSize + 1) - 1) / 2; - - if (!Number.isInteger(diagonalSize)) { - throw new TypeError( - `This array is not a compact representation of a Symmetric Matrix, ${JSON.stringify( - compact, - )}`, - ); - } - - const matrix = new SymmetricMatrix(diagonalSize); - for (let col = 0, row = 0, index = 0; index < compactSize; index++) { - matrix.set(col, row, compact[index]); - if (++col >= diagonalSize) col = ++row; - } - - return matrix; - } -} -SymmetricMatrix.prototype.klassType = 'SymmetricMatrix'; - -export class DistanceMatrix extends SymmetricMatrix { - /** - * not the same as matrix.isSymmetric() - * Here is to check if it's instanceof SymmetricMatrix without bundling issues - * - * @param value - * @returns {boolean} - */ - static isDistanceMatrix(value) { - return ( - SymmetricMatrix.isSymmetricMatrix(value) && - value.klassSubType === 'DistanceMatrix' - ); - } - - constructor(sideSize) { - super(sideSize); - - if (!this.isDistance()) { - throw new TypeError('Provided arguments do no produce a distance matrix'); - } - } - - set(rowIndex, columnIndex, value) { - // distance matrix diagonal is 0 - if (rowIndex === columnIndex) value = 0; - - return super.set(rowIndex, columnIndex, value); - } - - addCross(index, array) { - if (array === undefined) { - array = index; - index = this.diagonalSize; - } - - // ensure distance - array = array.slice(); - array[index] = 0; - - return super.addCross(index, array); - } - - toSymmetricMatrix() { - return new SymmetricMatrix(this); - } - - clone() { - const matrix = new DistanceMatrix(this.diagonalSize); - - for (const [row, col, value] of this.upperRightEntries()) { - if (row === col) continue; - matrix.set(row, col, value); - } - - return matrix; - } - - /** - * Compact format upper-right corner of matrix - * no diagonal (only zeros) - * iterable from left to right, from top to bottom. - * - * ``` - * A B C D - * A 0 1 2 3 - * B 1 0 4 5 - * C 2 4 0 6 - * D 3 5 6 0 - * ``` - * - * will return compact 1D array `[1, 2, 3, 4, 5, 6]` - * - * length is S(i=0, n=sideSize-1) => 6 for a 4 side sized matrix - * - * @returns {number[]} - */ - toCompact() { - const { diagonalSize } = this; - const compactLength = ((diagonalSize - 1) * diagonalSize) / 2; - - /** @type {number[]} */ - const compact = new Array(compactLength); - for (let col = 1, row = 0, index = 0; index < compact.length; index++) { - compact[index] = this.get(row, col); - - if (++col >= diagonalSize) col = ++row + 1; - } - - return compact; - } - - /** - * @param {number[]} compact - */ - static fromCompact(compact) { - const compactSize = compact.length; - // compactSize = (sideSize * (sideSize - 1)) / 2 - // sideSize = (Sqrt(8 × compactSize + 1) + 1) / 2 - const diagonalSize = (Math.sqrt(8 * compactSize + 1) + 1) / 2; - - if (!Number.isInteger(diagonalSize)) { - throw new TypeError( - `This array is not a compact representation of a DistanceMatrix, ${JSON.stringify( - compact, - )}`, - ); - } - - const matrix = new this(diagonalSize); - for (let col = 1, row = 0, index = 0; index < compactSize; index++) { - matrix.set(col, row, compact[index]); - if (++col >= diagonalSize) col = ++row + 1; - } - - return matrix; - } -} -DistanceMatrix.prototype.klassSubType = 'DistanceMatrix'; diff --git a/src/symmetricMatrix.js b/src/symmetricMatrix.js new file mode 100644 index 0000000..da6e672 --- /dev/null +++ b/src/symmetricMatrix.js @@ -0,0 +1,214 @@ +/** + * @typedef {0 | 1 | number | boolean} Mask + */ +import Matrix, { AbstractMatrix } from './matrix'; + +export class SymmetricMatrix extends AbstractMatrix { + /** @type {Matrix} */ + #matrix; + + get size() { + return this.#matrix.size; + } + + get rows() { + return this.#matrix.rows; + } + + get columns() { + return this.#matrix.columns; + } + + get diagonalSize() { + return this.rows; + } + + /** + * not the same as matrix.isSymmetric() + * Here is to check if it's instanceof SymmetricMatrix without bundling issues + * + * @param value + * @returns {boolean} + */ + static isSymmetricMatrix(value) { + return Matrix.isMatrix(value) && value.klassType === 'SymmetricMatrix'; + } + + /** + * @param diagonalSize + * @return {SymmetricMatrix} + */ + static zeros(diagonalSize) { + return new this(diagonalSize); + } + + /** + * @param diagonalSize + * @return {SymmetricMatrix} + */ + static ones(diagonalSize) { + return new this(diagonalSize).fill(1); + } + + /** + * @param {number | AbstractMatrix | ArrayLike>} diagonalSize + * @return {this} + */ + constructor(diagonalSize) { + super(); + + if (Matrix.isMatrix(diagonalSize)) { + if (!diagonalSize.isSymmetric()) { + throw new TypeError('not symmetric data'); + } + + this.#matrix = Matrix.copy( + diagonalSize, + new Matrix(diagonalSize.rows, diagonalSize.rows), + ); + } else if (Number.isInteger(diagonalSize) && diagonalSize >= 0) { + this.#matrix = new Matrix(diagonalSize, diagonalSize); + } else { + this.#matrix = new Matrix(diagonalSize); + + if (!this.isSymmetric()) { + throw new TypeError('not symmetric data'); + } + } + } + + clone() { + const matrix = new SymmetricMatrix(this.diagonalSize); + + for (const [row, col, value] of this.upperRightEntries()) { + matrix.set(row, col, value); + } + + return matrix; + } + + toMatrix() { + return new Matrix(this); + } + + get(rowIndex, columnIndex) { + return this.#matrix.get(rowIndex, columnIndex); + } + set(rowIndex, columnIndex, value) { + // symmetric set + this.#matrix.set(rowIndex, columnIndex, value); + this.#matrix.set(columnIndex, rowIndex, value); + + return this; + } + + removeCross(index) { + // symmetric remove side + this.#matrix.removeRow(index); + this.#matrix.removeColumn(index); + + return this; + } + + addCross(index, array) { + if (array === undefined) { + array = index; + index = this.diagonalSize; + } + + const row = array.slice(); + row.splice(index, 1); + + this.#matrix.addRow(index, row); + this.#matrix.addColumn(index, array); + + return this; + } + + /** + * @param {Mask[]} mask + */ + applyMask(mask) { + if (mask.length !== this.diagonalSize) { + throw new RangeError('Mask size do not match with matrix size'); + } + + // prepare sides to remove from matrix from mask + /** @type {number[]} */ + const sidesToRemove = []; + for (const [index, passthroughs] of mask.entries()) { + if (passthroughs) continue; + sidesToRemove.push(index); + } + // to remove from highest to lowest for no mutation shifting + sidesToRemove.reverse(); + + // remove sides + for (const sideIndex of sidesToRemove) { + this.removeCross(sideIndex); + } + + return this; + } + + /** + * Compact format upper-right corner of matrix + * iterate from left to right, from top to bottom. + * + * ``` + * A B C D + * A 1 2 3 4 + * B 2 5 6 7 + * C 3 6 8 9 + * D 4 7 9 10 + * ``` + * + * will return compact 1D array `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]` + * + * length is S(i=0, n=sideSize) => 10 for a 4 sideSized matrix + * + * @returns {number[]} + */ + toCompact() { + const { diagonalSize } = this; + + /** @type {number[]} */ + const compact = new Array((diagonalSize * (diagonalSize + 1)) / 2); + for (let col = 0, row = 0, index = 0; index < compact.length; index++) { + compact[index] = this.get(row, col); + + if (++col >= diagonalSize) col = ++row; + } + + return compact; + } + + /** + * @param {number[]} compact + * @return {SymmetricMatrix} + */ + static fromCompact(compact) { + const compactSize = compact.length; + // compactSize = (sideSize * (sideSize + 1)) / 2 + // https://mathsolver.microsoft.com/fr/solve-problem/y%20%3D%20%20x%20%60cdot%20%20%20%60frac%7B%20%20%60left(%20x%2B1%20%20%60right)%20%20%20%20%7D%7B%202%20%20%7D + // sideSize = (Sqrt(8 × compactSize + 1) - 1) / 2 + const diagonalSize = (Math.sqrt(8 * compactSize + 1) - 1) / 2; + + if (!Number.isInteger(diagonalSize)) { + throw new TypeError( + `This array is not a compact representation of a Symmetric Matrix, ${JSON.stringify( + compact, + )}`, + ); + } + + const matrix = new SymmetricMatrix(diagonalSize); + for (let col = 0, row = 0, index = 0; index < compactSize; index++) { + matrix.set(col, row, compact[index]); + if (++col >= diagonalSize) col = ++row; + } + + return matrix; + } +} +SymmetricMatrix.prototype.klassType = 'SymmetricMatrix'; From 8457dda354497af96f7b27a6a0ca469988634d1d Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:09:51 +0100 Subject: [PATCH 18/20] refactor: move upperRight iterators to SymmetricMatrix It has no real usage on non-symmetric matrix --- matrix.d.ts | 42 +++---- src/__tests__/matrix/iterators.test.js | 119 ------------------ .../symmetric_matrix/iterators.test.js | 28 +++++ src/matrix.js | 63 ---------- src/symmetricMatrix.js | 34 +++++ 5 files changed, 78 insertions(+), 208 deletions(-) create mode 100644 src/__tests__/symmetric_matrix/iterators.test.js diff --git a/matrix.d.ts b/matrix.d.ts index 9132767..76b5900 100644 --- a/matrix.d.ts +++ b/matrix.d.ts @@ -860,32 +860,6 @@ export abstract class AbstractMatrix { */ values(): Generator; - /** - * half iterator upper-right-corner from left to right, from top to bottom - * yield [row, column, value] - * @param [borderMax=this.rows] - clamp between Max(rows, columns) and 0. - * @param [missValue=0] - */ - upperRightEntries( - borderMax?: number, - missValue?: Miss | ((this: this, row: number, column: number) => Miss), - ): Generator< - [row: number, column: number, value: number | Miss], - void, - never - >; - - /** - * half iterator upper-right-corner from left to right, from top to bottom - * yield value - * @param [borderMax=this.rows] - clamp between Max(rows, columns) and 0. - * @param [missValue=0] - */ - upperRightValues( - borderMax?: number, - missValue?: Miss | ((this: this, row: number, column: number) => Miss), - ): Generator; - // From here we document methods dynamically generated from operators // Mathematical operators @@ -1142,6 +1116,22 @@ export class SymmetricMatrix extends Matrix { static fromCompact(compact: number[]): SymmetricMatrix; clone(): this; + + /** + * half iterator upper-right-corner from left to right, from top to bottom + * yield [row, column, value] + */ + upperRightEntries(): Generator< + [row: number, column: number, value: number], + void, + never + >; + + /** + * half iterator upper-right-corner from left to right, from top to bottom + * yield value + */ + upperRightValues(): Generator; } export class DistanceMatrix extends SymmetricMatrix { diff --git a/src/__tests__/matrix/iterators.test.js b/src/__tests__/matrix/iterators.test.js index 4444c08..f057f4f 100644 --- a/src/__tests__/matrix/iterators.test.js +++ b/src/__tests__/matrix/iterators.test.js @@ -47,123 +47,4 @@ describe('iterators methods', () => { ]); expect(Array.from(rect.values())).toStrictEqual([0, 1, 2, 3, 4, 5, 6, 7]); }); - - it('upperRightEntries simple', () => { - expect(Array.from(square.upperRightEntries())).toStrictEqual([ - [0, 0, 0], - [0, 1, 1], - [0, 2, 2], - [1, 1, 4], - [1, 2, 5], - [2, 2, 8], - ]); - - // on no borderMax, fallback on number of rows, so on a wide rectangle, it reduce read to inset left-up square matrix - expect(Array.from(rect.upperRightEntries())).toStrictEqual([ - [0, 0, 0], - [0, 1, 1], - [1, 1, 5], - ]); - }); - it('upperRightEntries expand columns', () => { - expect(Array.from(rect.upperRightEntries(rect.columns))).toStrictEqual([ - [0, 0, 0], - [0, 1, 1], - [0, 2, 2], - [0, 3, 3], - [1, 1, 5], - [1, 2, 6], - [1, 3, 7], - [2, 2, 0], - [2, 3, 0], - [3, 3, 0], - ]); - }); - it('upperRightEntries expand columns with default values', () => { - const value = Array.from(rect.upperRightEntries(rect.columns, '∅')); - const expected = [ - [0, 0, 0], - [0, 1, 1], - [0, 2, 2], - [0, 3, 3], - [1, 1, 5], - [1, 2, 6], - [1, 3, 7], - [2, 2, '∅'], - [2, 3, '∅'], - [3, 3, '∅'], - ]; - - expect(value).toStrictEqual(expected); - }); - it('upperRightEntries expand columns with computed default values', () => { - const value = Array.from( - rect.upperRightEntries( - rect.columns, - function index1DVirtualColumnExpand(row, col) { - return row * this.columns + col; - }, - ), - ); - const expected = [ - [0, 0, 0], - [0, 1, 1], - [0, 2, 2], - [0, 3, 3], - [1, 1, 5], - [1, 2, 6], - [1, 3, 7], - [2, 2, 10], - [2, 3, 11], - [3, 3, 15], - ]; - - expect(value).toStrictEqual(expected); - }); - it('upperRightEntries square expand between rows and columns', () => { - const value = Array.from(rect.upperRightEntries(3)); - const expected = [ - [0, 0, 0], - [0, 1, 1], - [0, 2, 2], - [1, 1, 5], - [1, 2, 6], - [2, 2, 0], - ]; - - expect(value).toStrictEqual(expected); - }); - - it('upperRightValues simple', () => { - expect(Array.from(square.upperRightValues())).toStrictEqual([ - 0, 1, 2, 4, 5, 8, - ]); - - expect(Array.from(rect.upperRightValues())).toStrictEqual([0, 1, 5]); - }); - it('upperRightValues expand columns with default values', () => { - const value = Array.from(rect.upperRightValues(rect.columns, '∅')); - const expected = [0, 1, 2, 3, 5, 6, 7, '∅', '∅', '∅']; - - expect(value).toStrictEqual(expected); - }); - it('upperRightValues expand columns with computed default values', () => { - const value = Array.from( - rect.upperRightValues( - rect.columns, - function index1DVirtualColumnExpand(row, col) { - return row * this.columns + col; - }, - ), - ); - const expected = [0, 1, 2, 3, 5, 6, 7, 10, 11, 15]; - - expect(value).toStrictEqual(expected); - }); - it('upperRightValues square expand between rows and columns', () => { - const value = Array.from(rect.upperRightValues(3)); - const expected = [0, 1, 2, 5, 6, 0]; - - expect(value).toStrictEqual(expected); - }); }); diff --git a/src/__tests__/symmetric_matrix/iterators.test.js b/src/__tests__/symmetric_matrix/iterators.test.js new file mode 100644 index 0000000..36415a1 --- /dev/null +++ b/src/__tests__/symmetric_matrix/iterators.test.js @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest'; + +import { SymmetricMatrix } from '../../index'; + +describe('iterators methods', () => { + const square = new SymmetricMatrix([ + [0, 1, 2], + [1, 4, 5], + [2, 5, 8], + ]); + + it('upperRightEntries', () => { + expect(Array.from(square.upperRightEntries())).toStrictEqual([ + [0, 0, 0], + [0, 1, 1], + [0, 2, 2], + [1, 1, 4], + [1, 2, 5], + [2, 2, 8], + ]); + }); + + it('upperRightValues', () => { + expect(Array.from(square.upperRightValues())).toStrictEqual([ + 0, 1, 2, 4, 5, 8, + ]); + }); +}); diff --git a/src/matrix.js b/src/matrix.js index 68f6db6..8514125 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -1552,69 +1552,6 @@ export class AbstractMatrix { } } } - - /** - * half iterator upper-right-corner from left to right, from top to bottom - * yield [row, column, value] - * - * @param {number} [borderMax = this.rows] - if specified will check if <= to rows and columns - * @param {any} [missValue = 0] - number or function returning a number - * - * @returns {Generator<[number, number, number], void, *>} - */ - *upperRightEntries(borderMax = this.rows, missValue = 0) { - borderMax = Math.min( - Math.max(this.rows, this.columns), - Math.max(0, borderMax), - ); - - const isMissValueFunction = typeof missValue?.call === 'function'; - - for (let row = 0, col = 0; row < borderMax; void 0) { - const value = - row >= this.rows || col >= this.columns - ? isMissValueFunction - ? missValue.call(this, row, col) - : missValue - : this.get(row, col); - - yield [row, col, value]; - - // at the end of row, move cursor to next row at diagonal position - if (++col >= borderMax) col = ++row; - } - } - - /** - * half iterator upper-right-corner from left to right, from top to bottom - * yield value - * - * @param {number} [borderMax = this.rows] - if specified will check if <= to rows and columns - * @param [missValue = 0] - number or function returning a number - * - * @returns {Generator<[number, number, number], void, *>} - */ - *upperRightValues(borderMax = this.rows, missValue = 0) { - borderMax = Math.min( - Math.max(this.rows, this.columns), - Math.max(0, borderMax), - ); - const isMissValueFunction = typeof missValue?.call === 'function'; - - for (let row = 0, col = 0; row < borderMax; void 0) { - const value = - row >= this.rows || col >= this.columns - ? isMissValueFunction - ? missValue.call(this, row, col) - : missValue - : this.get(row, col); - - yield value; - - // at the end of row, move cursor to next row at diagonal position - if (++col >= borderMax) col = ++row; - } - } } AbstractMatrix.prototype.klass = 'Matrix'; diff --git a/src/symmetricMatrix.js b/src/symmetricMatrix.js index da6e672..87c5f76 100644 --- a/src/symmetricMatrix.js +++ b/src/symmetricMatrix.js @@ -210,5 +210,39 @@ export class SymmetricMatrix extends AbstractMatrix { return matrix; } + + /** + * half iterator upper-right-corner from left to right, from top to bottom + * yield [row, column, value] + * + * @returns {Generator<[number, number, number], void, *>} + */ + *upperRightEntries() { + for (let row = 0, col = 0; row < this.diagonalSize; void 0) { + const value = this.get(row, col); + + yield [row, col, value]; + + // at the end of row, move cursor to next row at diagonal position + if (++col >= this.diagonalSize) col = ++row; + } + } + + /** + * half iterator upper-right-corner from left to right, from top to bottom + * yield value + * + * @returns {Generator<[number, number, number], void, *>} + */ + *upperRightValues() { + for (let row = 0, col = 0; row < this.diagonalSize; void 0) { + const value = this.get(row, col); + + yield value; + + // at the end of row, move cursor to next row at diagonal position + if (++col >= this.diagonalSize) col = ++row; + } + } } SymmetricMatrix.prototype.klassType = 'SymmetricMatrix'; From 534c30be0d664148960d75b57a804ad7cc3ee06f Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:58:54 +0100 Subject: [PATCH 19/20] fix: typings from refactor --- matrix.d.ts | 68 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/matrix.d.ts b/matrix.d.ts index 76b5900..c3a3c2f 100644 --- a/matrix.d.ts +++ b/matrix.d.ts @@ -170,9 +170,13 @@ export abstract class AbstractMatrix { * This is equivalent to calling the Matrix constructor. * @param rows - Number of rows. * @param columns - Number of columns. + * @template _M is private. Don't override it. * @returns The new matrix. */ - static zeros(rows: number, columns: number): Matrix; + static zeros<_M extends AbstractMatrix = Matrix>( + rows: number, + columns: number, + ): _M; /** * Creates a matrix with the given dimensions. Values will be set to one. @@ -180,7 +184,10 @@ export abstract class AbstractMatrix { * @param columns - Number of columns. * @returns The new matrix. */ - static ones(rows: number, columns: number): Matrix; + static ones( + rows: number, + columns: number, + ): M; /** * Creates a matrix with the given dimensions. Values will be randomly set. @@ -1009,28 +1016,11 @@ export class Matrix extends AbstractMatrix { export default Matrix; -export class SymmetricMatrix extends Matrix { - /** - * Creates a symmetric matrix with the given dimensions. Values will be set to zero. - * This is equivalent to calling the Matrix constructor. - * @param diagonalSize - Number of rows or columns (square). - * @returns The new symmetric matrix. - */ - static zeros(diagonalSize: number): SymmetricMatrix; - - /** - * Creates a symmetric matrix with the given dimensions. Values will be set to one. - * @param diagonalSize - Number of rows or columns (square). - * @returns The new symmetric matrix. - */ - static ones(diagonalSize: number): SymmetricMatrix; - - static isSymmetricMatrix(value: unknown): value is SymmetricMatrix; - +export class SymmetricMatrix extends AbstractMatrix { /** * alias for `rows` or `columns` (square matrix so equals) */ - get diagonalSize(): number; + readonly diagonalSize: number; /** * @throws TypeError if otherMatrix is not symmetric @@ -1044,6 +1034,32 @@ export class SymmetricMatrix extends Matrix { */ constructor(data: ArrayLike>); + get(rowIndex: number, columnIndex: number): number; + set(rowIndex: number, columnIndex: number, value: number): this; + + /** + * Creates a symmetric matrix with the given dimensions. Values will be set to zero. + * This is equivalent to calling the Matrix constructor. + * + * @param diagonalSize - Number of rows or columns (square). + * @template _M is private, do not override it. + * @returns The new symmetric matrix. + */ + static zeros<_M extends AbstractMatrix = SymmetricMatrix>( + diagonalSize: number, + ): _M; + /** + * Creates a symmetric matrix with the given dimensions. Values will be set to one. + * @param diagonalSize - Number of rows or columns (square). + * @template _M is private, do not override it. + * @returns The new symmetric matrix. + */ + static ones<_M extends AbstractMatrix = SymmetricMatrix>( + diagonalSize: number, + ): _M; + + static isSymmetricMatrix(value: unknown): value is SymmetricMatrix; + /** * copy to a new matrix */ @@ -1139,16 +1155,22 @@ export class DistanceMatrix extends SymmetricMatrix { * Creates a distance matrix with the given dimensions. Values will be set to zero. * This is equivalent to calling the Matrix constructor. * @param sidesSize - Number of rows or columns (square). + * @template _M is private, do not specify it * @returns The new symmetric matrix. */ - static zeros(sidesSize: number): DistanceMatrix; + static zeros<_M extends AbstractMatrix = DistanceMatrix>( + sidesSize: number, + ): _M; /** * Creates a symmetric matrix with the given dimensions. Values will be set to one. * @param sidesSize - Number of rows or columns (square). + * @template _M is private, do not specify it * @returns The new symmetric matrix. */ - static ones(sidesSize: number): DistanceMatrix; + static ones<_M extends AbstractMatrix = DistanceMatrix>( + sidesSize: number, + ): _M; static isDistanceMatrix(value: unknown): value is DistanceMatrix; From f8c15189b439ea4807bb2c2a9de793f8e2c5cd68 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Thu, 30 Nov 2023 09:03:53 +0100 Subject: [PATCH 20/20] fix: forgot isDistance signature in .d.ts --- matrix.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/matrix.d.ts b/matrix.d.ts index c3a3c2f..20a1e5d 100644 --- a/matrix.d.ts +++ b/matrix.d.ts @@ -326,6 +326,11 @@ export abstract class AbstractMatrix { */ isSquare(): boolean; + /** + * Returns whether the matrix is symmetric and diagonal values are equals to 0 + */ + isDistance(): boolean; + /** * Returns whether the number of rows or columns (or both) is zero. */