Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,18 @@ var z = Matrix.ones(2, 3); // z = Matrix [[1, 1, 1], [1, 1, 1], rows: 2, column
var z = Matrix.eye(3, 4); // z = Matrix [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], rows: 3, columns: 4]. there are 1 only in the diagonal
```

#### Concatenation of matrices
```js
var M = new Matrix([
[1, 2],
[3, 4],
]);

var stacked = M.concat([[5, 6]]); // stacked = Matrix [[1, 2], [3, 4], [5, 6], rows: 3, columns: 2]
var widened = M.concat(Matrix.columnVector([5, 6]), 'column'); // widened = Matrix [[1, 2, 5], [3, 4, 6], rows: 2, columns: 3]
```
Concatenating by row needs the same number of columns on both sides, concatenating by column needs the same number of rows. The two operands are left untouched.

### Maths
```js
const {
Expand Down
9 changes: 9 additions & 0 deletions matrix.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,15 @@ export abstract class AbstractMatrix {
startColumn: number,
): this;

/**
* Returns a new matrix made of this matrix followed by the other one.
* Concatenating by 'row' stacks the two matrices vertically and requires the same number of columns.
* Concatenating by 'column' stacks them side by side and requires the same number of rows.
* @param other - The matrix to append.
* @param by - Concatenate by 'row' or 'column'. Default: `'row'`.
*/
concat(other: MaybeMatrix, by?: MatrixDimension): Matrix;

/**
* Return a new matrix based on a selection of rows and columns.
* Order of the indices matters and the same index can be used more than once.
Expand Down
184 changes: 184 additions & 0 deletions src/__tests__/matrix/concat.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { describe, it, expect } from 'vitest';

import { Matrix, MatrixTransposeView, SymmetricMatrix } from '../..';

describe('concat', () => {
const matrix = new Matrix([
[1, 2],
[3, 4],
]);
const other = new Matrix([
[5, 6],
[7, 8],
[9, 10],
]);

it('by row stacks the matrices vertically', () => {
const result = matrix.concat(other, 'row');
expect(result.rows).toBe(5);
expect(result.columns).toBe(2);
expect(result.to2DArray()).toStrictEqual([
[1, 2],
[3, 4],
[5, 6],
[7, 8],
[9, 10],
]);
});

it('by column stacks the matrices side by side', () => {
const result = matrix.concat(other.transpose(), 'column');
expect(result.rows).toBe(2);
expect(result.columns).toBe(5);
expect(result.to2DArray()).toStrictEqual([
[1, 2, 5, 7, 9],
[3, 4, 6, 8, 10],
]);
});

it('concatenates by row when the dimension is omitted', () => {
expect(matrix.concat(other).to2DArray()).toStrictEqual(
matrix.concat(other, 'row').to2DArray(),
);
});

it('accepts a 2D array', () => {
expect(matrix.concat([[5, 6]]).to2DArray()).toStrictEqual([
[1, 2],
[3, 4],
[5, 6],
]);
});

it('leaves both operands untouched', () => {
matrix.concat(other, 'row');
expect(matrix.to2DArray()).toStrictEqual([
[1, 2],
[3, 4],
]);
expect(other.rows).toBe(3);
});

it('always returns a plain matrix', () => {
const symmetric = new SymmetricMatrix([
[1, 2],
[2, 3],
]);
const result = symmetric.concat(matrix, 'row');
expect(result).toBeInstanceOf(Matrix);
expect(result.to2DArray()).toStrictEqual([
[1, 2],
[2, 3],
[1, 2],
[3, 4],
]);
});

it('reads a view along the view dimensions', () => {
const view = new MatrixTransposeView(
new Matrix([
[1, 2, 3],
[4, 5, 6],
]),
);
expect(view.concat([[7, 8]], 'row').to2DArray()).toStrictEqual([
[1, 4],
[2, 5],
[3, 6],
[7, 8],
]);
});

it('appends a column vector', () => {
const result = matrix.concat(Matrix.columnVector([5, 6]), 'column');
expect(result.to2DArray()).toStrictEqual([
[1, 2, 5],
[3, 4, 6],
]);
});

it('appends a row vector', () => {
const result = matrix.concat(Matrix.rowVector([5, 6]), 'row');
expect(result.to2DArray()).toStrictEqual([
[1, 2],
[3, 4],
[5, 6],
]);
});

it('chains to gather several matrices', () => {
const result = matrix
.concat([[5, 6]], 'row')
.concat(Matrix.columnVector([7, 8, 9]), 'column');
expect(result.to2DArray()).toStrictEqual([
[1, 2, 7],
[3, 4, 8],
[5, 6, 9],
]);
});
});

describe('concat with degenerate matrices', () => {
it('by row with a matrix without rows', () => {
const result = new Matrix(0, 2).concat(new Matrix([[1, 2]]), 'row');
expect(result.to2DArray()).toStrictEqual([[1, 2]]);
});

it('by row onto a matrix without rows', () => {
const result = new Matrix([[1, 2]]).concat(new Matrix(0, 2), 'row');
expect(result.to2DArray()).toStrictEqual([[1, 2]]);
});

it('by column with a matrix without columns', () => {
const result = new Matrix(2, 0).concat(new Matrix([[1], [2]]), 'column');
expect(result.to2DArray()).toStrictEqual([[1], [2]]);
});

it('by column onto a matrix without columns', () => {
const result = new Matrix([[1], [2]]).concat(new Matrix(2, 0), 'column');
expect(result.to2DArray()).toStrictEqual([[1], [2]]);
});

it('by column of two matrices without rows', () => {
const result = new Matrix(0, 1).concat(new Matrix(0, 2), 'column');
expect(result.rows).toBe(0);
expect(result.columns).toBe(3);
});

it('by row of two matrices without columns', () => {
const result = new Matrix(1, 0).concat(new Matrix(2, 0), 'row');
expect(result.rows).toBe(3);
expect(result.columns).toBe(0);
});

it('by row of two 0x0 matrices', () => {
const result = new Matrix(0, 0).concat(new Matrix(0, 0), 'row');
expect(result.rows).toBe(0);
expect(result.columns).toBe(0);
});
});

describe('concat error handling', () => {
const matrix = new Matrix([
[1, 2],
[3, 4],
]);

it('throws when the number of columns differs', () => {
expect(() => matrix.concat(new Matrix([[1, 2, 3]]), 'row')).toThrow(
/^both matrices must have the same number of columns$/,
);
});

it('throws when the number of rows differs', () => {
expect(() => matrix.concat(new Matrix([[1, 2, 3]]), 'column')).toThrow(
/^both matrices must have the same number of rows$/,
);
});

it('throws when the dimension is unknown', () => {
expect(() => matrix.concat(matrix, 'diagonal')).toThrow(
/^invalid option: diagonal$/,
);
});
});
30 changes: 30 additions & 0 deletions src/matrix.js
Original file line number Diff line number Diff line change
Expand Up @@ -1420,6 +1420,36 @@ export class AbstractMatrix {
return this;
}

concat(other, by = 'row') {
other = Matrix.checkMatrix(other);
switch (by) {
case 'row': {
if (this.columns !== other.columns) {
throw new RangeError(
'both matrices must have the same number of columns',
);
}
const result = new Matrix(this.rows + other.rows, this.columns);
result.setSubMatrix(this, 0, 0);
result.setSubMatrix(other, this.rows, 0);
return result;
}
case 'column': {
if (this.rows !== other.rows) {
throw new RangeError(
'both matrices must have the same number of rows',
);
}
const result = new Matrix(this.rows, this.columns + other.columns);
result.setSubMatrix(this, 0, 0);
result.setSubMatrix(other, 0, this.columns);
return result;
}
default:
throw new Error(`invalid option: ${by}`);
}
}

selection(rowIndices, columnIndices) {
checkRowIndices(this, rowIndices);
checkColumnIndices(this, columnIndices);
Expand Down