Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
2895aaa
feat: utils/Matrix.ts to contain helper functions that do not belong …
weiseng18 Aug 7, 2021
e9c8898
feat: create util range
weiseng18 Aug 7, 2021
e82a8b5
test: create test for isZeroRow
weiseng18 Aug 7, 2021
98d3196
chore: add script for unit tests
weiseng18 Aug 7, 2021
0dc9bba
test: also test range()
weiseng18 Aug 7, 2021
a064ca2
feat: create util leadingEntryIndex
weiseng18 Aug 7, 2021
88ef2a1
test: create test for leadingEntryIndex
weiseng18 Aug 7, 2021
e05e02b
chore: move utils test into unit folder
weiseng18 Aug 7, 2021
8ef288f
chore: reorganize unit test to run from index file
weiseng18 Aug 7, 2021
be730b0
feat: create enum EchelonType
weiseng18 Aug 7, 2021
40eae32
feat: create method echelonStatus
weiseng18 Aug 7, 2021
2883a97
test: create matrix unit test file
weiseng18 Aug 7, 2021
0292d76
chore: add unit test to github actions
weiseng18 Aug 7, 2021
bb313e1
chore: remove push github actions trigger
weiseng18 Aug 7, 2021
cc7c4de
test: run chai.expect for each subtest rather than on a .every
weiseng18 Aug 7, 2021
c584e18
feat: create multiplyRow and test
weiseng18 Aug 7, 2021
e83a6cf
feat: create swapRows and test
weiseng18 Aug 7, 2021
c1c45f7
feat: create addMultiple and test
weiseng18 Aug 7, 2021
7b7e2d5
fix: leading entry logic error
weiseng18 Aug 7, 2021
1a21e7e
feat: create toREF and test
weiseng18 Aug 7, 2021
2c76a5d
feat: create toRREF and test
weiseng18 Aug 7, 2021
5274517
feat: user needs to specify action in frontend query
weiseng18 Aug 7, 2021
f74fc0d
refactor: create frontend state command and add comments to states
weiseng18 Aug 7, 2021
66b40de
fix: set initial state of inputArray to be empty 2D array
weiseng18 Aug 7, 2021
54c9249
feat: create controller function reduceRREF
weiseng18 Aug 7, 2021
7fdc564
feat: create endpoint for rref
weiseng18 Aug 7, 2021
28d803c
feat: handle rref in frontend
weiseng18 Aug 7, 2021
2e115ac
feat: only perform swap if necessary
weiseng18 Aug 7, 2021
15abd17
feat: add initial state to actions, and also record matrix state
weiseng18 Aug 7, 2021
8ab5e19
chore: install lodash
weiseng18 Aug 7, 2021
d4bc87a
fix: apply deep clone on entries
weiseng18 Aug 7, 2021
09fcef4
feat: add y-margin
weiseng18 Aug 7, 2021
3ad2daf
feat: show rref steps
weiseng18 Aug 7, 2021
e3913f6
feat: do not multiply if factor is 1
weiseng18 Aug 7, 2021
4469de5
fix: issues with justifyContent and overflow
weiseng18 Aug 8, 2021
c2c72e9
feat: put $$ around params for inline math
weiseng18 Aug 8, 2021
2e249cd
fix: make inline math actually render inline with Text
weiseng18 Aug 8, 2021
09797bf
feat: layout improvements
weiseng18 Aug 8, 2021
9e66151
feat: add matrix row operation notation
weiseng18 Aug 8, 2021
6a07b45
feat: put or inside the brackets
weiseng18 Aug 8, 2021
e2d8233
feat: add vertical spacing between intermediate steps
weiseng18 Aug 8, 2021
7f0846a
refactor: slitp addMultiple action into positive and negative cases
weiseng18 Aug 8, 2021
8945bf1
feat: trigger frontend query params upon submit
weiseng18 Aug 8, 2021
4c69340
fix: grammar
weiseng18 Aug 8, 2021
361e310
feat: if preparing to trigger submit, disable submit button
weiseng18 Aug 8, 2021
5fd9782
feat: prevent click submit if page is still loading, or handleSubmit …
weiseng18 Aug 8, 2021
0ab3e7f
feat: add spinner when is loading
weiseng18 Aug 8, 2021
9a60b8e
feat: layout improvements
weiseng18 Aug 8, 2021
b2da509
fix: prevent query from resubmitting upon each keypress
weiseng18 Aug 8, 2021
4ea2316
feat: layout adjustment for mobile screens
weiseng18 Aug 8, 2021
89a0ec4
feat: set maxW so that desktop screens look reasonable
weiseng18 Aug 8, 2021
034c665
fix: ios scrolling
weiseng18 Aug 8, 2021
5121271
fix: make components rerender upon changing command
weiseng18 Aug 8, 2021
b049a2d
feat: if unrecognized command, clear screen and show error
weiseng18 Aug 8, 2021
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
25 changes: 22 additions & 3 deletions .github/workflows/github-actions.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
name: Github Actions
on:
push:
pull_request:
jobs:
test:
name: Test
test-api:
name: test-api
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
Expand All @@ -23,3 +22,23 @@ jobs:
${{ runner.OS }}-
- run: npm ci
- run: npm run test
test-unit:
name: test-unit
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: "14.x"
- name: Cache Node.js modules
uses: actions/cache@v2
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.OS }}-node-
${{ runner.OS }}-
- run: npm ci
- run: npm run test-unit
214 changes: 214 additions & 0 deletions app/classes/Matrix.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import { range } from "../utils/misc"

import { EchelonType } from "../types/Matrix"
import { leadingEntryIndex } from "../utils/Matrix"

import * as _ from "lodash"

interface IMatrix {
rows: number
columns: number
Expand All @@ -23,6 +30,213 @@ abstract class BaseMatrix {
this.entries = tmp
}
}

/**
* Returns whether a matrix is in RREF, REF, or NONE.
*/
echelonStatus(): EchelonType {
// obtain array of leading entry indices
let leadingIndices: number[] = []
this.entries.forEach((row) => {
const leadingIndex = leadingEntryIndex(row)
leadingIndices.push(leadingIndex)
})

// remove all end -1s as they are extra zero rows at the bottom of the matrix
while (
leadingIndices.length > 0 &&
leadingIndices[leadingIndices.length - 1] === -1
)
leadingIndices.pop()

// Check 1a: if there are still non-zero rows, this means they are not at the bottom, so return NONE
if (leadingIndices.some((one) => one === -1)) return EchelonType.NONE

// Check 1b: if leadingEntryIndices not in increasing order, return NONE
let idx = 1
while (idx < this.rows) {
if (leadingIndices[idx - 1] >= leadingIndices[idx])
return EchelonType.NONE
idx++
}

// reaching here means is either REF or RREF

// Check 2: if leadingIndices has no more entries (i.e. was all -1s), this is a zero matrix, and is RREF
if (leadingIndices.length === 0) return EchelonType.RREF

// Check 3: For RREF, all leading entries of nonzero rows should be 1
const allLeadingOne = leadingIndices.every(
(colIdx, rowIdx) => this.entries[rowIdx][colIdx] === 1
)
if (!allLeadingOne) return EchelonType.REF

// Check 4: For RREF, the only nonzero value in a pivot column is the pivot point
const pivotColumnsCheck = leadingIndices.every((colIdx, idx) => {
const res = range(this.rows).every((rowIdx) => {
// if the leading index found previously belongs to this row
if (idx == rowIdx) return true

return this.entries[rowIdx][colIdx] === 0
})
return res
})
if (!pivotColumnsCheck) return EchelonType.REF

// reaching here means RREF
return EchelonType.RREF
}

/**
* Multiplies a row by a given factor
* @param rowIdx row index number
* @param factor factor to multiply by
*/
multiplyRow(rowIdx: number, factor: number) {
this.entries[rowIdx] = this.entries[rowIdx].map((one) => factor * one)
}

/**
* Swaps rows i and j
* @param i row index number
* @param j row index number
*/
swapRows(i: number, j: number) {
const tmp = this.entries[i]
this.entries[i] = this.entries[j]
this.entries[j] = tmp
}

/**
* Adds a multiple of one row to another row
* @param rowIdx index of the row to multiply factor
* @param factor factor to multiply by
* @param addToIdx index of the row to add to
*/
addMultiple(rowIdx: number, factor: number, addToIdx: number) {
range(this.columns).forEach((colIdx) => {
this.entries[addToIdx][colIdx] += this.entries[rowIdx][colIdx] * factor
})
}

/**
* Returns true if every entry in column colIdx is 0, false otherwise
* @param colIdx index of the column
*/
isZeroColumn(colIdx: number) {
return range(this.rows).every(
(rowIdx) => this.entries[rowIdx][colIdx] === 0
)
}

/**
* Reduces the matrix to REF
*/
toREF() {
let actions = [] // array of actions done during REF

actions.push({
action: "none",
params: [],
matrix: _.cloneDeep(this.entries),
})

let colIdx = 0 // leftmost nonzero column idx
let rowsDone = 0 // number of rows done

while (this.echelonStatus() === EchelonType.NONE) {
// find next nonzero column
let foundColumn = false
while (!foundColumn && colIdx < this.columns) {
if (this.isZeroColumn(colIdx)) colIdx++
else foundColumn = true
}

if (foundColumn) {
let firstEntryRowIdx = -1
// find first nonzero entry in column, that is excluding the done rows
for (let i = rowsDone; i < this.rows; i++)
if (this.entries[i][colIdx] !== 0) {
firstEntryRowIdx = i
break
}

// swap top row with this row
if (rowsDone !== firstEntryRowIdx) {
this.swapRows(rowsDone, firstEntryRowIdx)
actions.push({
action: "swap",
params: [rowsDone, firstEntryRowIdx],
matrix: _.cloneDeep(this.entries),
})
}

// add multiple of top row to every other row below
const pivotValue = this.entries[rowsDone][colIdx]
for (let i = rowsDone + 1; i < this.rows; i++) {
const factor = (this.entries[i][colIdx] / pivotValue) * -1
this.addMultiple(rowsDone, factor, i)
actions.push({
action: "addMultiple",
params: [rowsDone, factor, i],
matrix: _.cloneDeep(this.entries),
})
}

// mark as done
rowsDone++
colIdx++
} else {
break
}
}

return actions
}

/**
* Reduces the matrix to RREF
*/
toRREF() {
let actions = this.toREF()

// make all leading entries 1
this.entries.forEach((row, rowIdx) => {
const colIdx = leadingEntryIndex(row)
if (colIdx !== -1) {
const leadingVal = row[colIdx]
const factor = 1 / leadingVal
if (factor !== 1) {
this.multiplyRow(rowIdx, factor)
actions.push({
action: "multiplyRow",
params: [rowIdx, factor],
matrix: _.cloneDeep(this.entries),
})
}
}
})

// add multiples to rows above
for (let i = this.rows - 1; i >= 0; i--) {
const colIdx = leadingEntryIndex(this.entries[i])
if (colIdx === -1) continue

for (let j = i - 1; j >= 0; j--) {
const factor = -this.entries[j][colIdx]
if (factor !== 0) {
this.addMultiple(i, factor, j)
actions.push({
action: "addMultiple",
params: [i, factor, j],
matrix: _.cloneDeep(this.entries),
})
}
}
}

return actions
}
}

class Matrix extends BaseMatrix {
Expand Down
30 changes: 29 additions & 1 deletion app/controllers/matrix.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { VercelRequest, VercelResponse } from "@vercel/node"

import { SquareMatrix } from "../classes/Matrix"
import { Matrix, SquareMatrix } from "../classes/Matrix"

const calcDeterminant = (req: VercelRequest, res: VercelResponse) => {
try {
Expand All @@ -27,6 +27,34 @@ const calcDeterminant = (req: VercelRequest, res: VercelResponse) => {
}
}

const reduceRREF = (req: VercelRequest, res: VercelResponse) => {
try {
let arr
// string array
if (Array.isArray(req.query.matrix))
arr = JSON.parse(req.query.matrix.join())
else arr = JSON.parse(req.query.matrix)

const rows = arr.length
const cols = arr[0].length

const matrix = new Matrix({
rows,
columns: cols,
entries: arr,
})

const actions = matrix.toRREF()
res.json({
actions,
matrix: matrix.entries,
})
} catch (err) {
res.status(500).json({ message: err.message })
}
}

export default {
calcDeterminant,
reduceRREF,
}
7 changes: 7 additions & 0 deletions app/tests/unit/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import utils from "./utils"
import matrix from "./matrix"

describe("Unit tests", () => {
describe("Utils tests", utils)
describe("Matrix tests", matrix)
})
Loading