15 files changed
+718
-195
No files matched your search
@@ -1,3 +1,16 @@
|
|||||||
|
# 1.3.0
|
||||||
|
- iterable
|
||||||
|
- Add Prettify helper type
|
||||||
|
- structures
|
||||||
|
- Add BinaryHeap, Memo, and StateMachine
|
||||||
|
- Split into multiple files
|
||||||
|
- Matrix
|
||||||
|
- Matrix type now tracks its dimensions
|
||||||
|
- graph
|
||||||
|
- graph-solver
|
||||||
|
- Add Kruskal's algorithm for finding a minimum spanning tree.
|
||||||
|
|
||||||
|
|
||||||
# 1.2.0
|
# 1.2.0
|
||||||
|
|
||||||
- core
|
- core
|
||||||
|
|||||||
@@ -143,3 +143,4 @@ export function pinkyPromise<T>(): [
|
|||||||
// the assignment to `p`
|
// the assignment to `p`
|
||||||
return [p, resolve!, reject!]
|
return [p, resolve!, reject!]
|
||||||
}
|
}
|
||||||
|
|
||||||
+4
-1
@@ -63,7 +63,10 @@ export function html(strings: TemplateStringsArray | string, ...exprs: unknown[]
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a text node for use in the DOM. The text content is NOT escaped.
|
* Creates a text node for use in the DOM.
|
||||||
|
* @remarks
|
||||||
|
* The text content is escaped using
|
||||||
|
* the browser's built-in HTML escaping via `document.createTextNode`.
|
||||||
* @author MindfulMinun
|
* @author MindfulMinun
|
||||||
* @since 2022-06-04
|
* @since 2022-06-04
|
||||||
*/
|
*/
|
||||||
|
|||||||
+3
-1
@@ -7,7 +7,9 @@ export type Repeated<
|
|||||||
T,
|
T,
|
||||||
N extends number,
|
N extends number,
|
||||||
R extends readonly T[] = [],
|
R extends readonly T[] = [],
|
||||||
> = R['length'] extends N ? R : Repeated<T, N, readonly [T, ...R]>;
|
> = R['length'] extends N ? R : Repeated<T, N, readonly [T, ...R]>
|
||||||
|
|
||||||
|
export type Prettify<T> = { [K in keyof T]: T[K] } & {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an iterable that always yields the given value.
|
* Creates an iterable that always yields the given value.
|
||||||
|
|||||||
+7
-2
@@ -2,6 +2,7 @@ import { g1, g2, g3 } from '../tools/graph/_graph-samples.ts'
|
|||||||
import { GraphSolver } from '../tools/graph/graph-solver.ts'
|
import { GraphSolver } from '../tools/graph/graph-solver.ts'
|
||||||
import { assertEquals } from "https://deno.land/std/testing/asserts.ts"
|
import { assertEquals } from "https://deno.land/std/testing/asserts.ts"
|
||||||
import { Path } from "../tools/graph/graph.ts"
|
import { Path } from "../tools/graph/graph.ts"
|
||||||
|
import { dedent } from "../core/string.ts"
|
||||||
|
|
||||||
// TODO: Add more tests!
|
// TODO: Add more tests!
|
||||||
|
|
||||||
@@ -64,7 +65,11 @@ function assertPathsEqual<vData, eData>(
|
|||||||
|
|
||||||
expectedV.forEach((v, i) => assertEquals(path?.vertices[i].id, v))
|
expectedV.forEach((v, i) => assertEquals(path?.vertices[i].id, v))
|
||||||
expectedE.forEach((e, i) => assertEquals(path?.edges[i].id, e))
|
expectedE.forEach((e, i) => assertEquals(path?.edges[i].id, e))
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
throw Error(`Paths are different! Expected: [${expectedV.join(' -> ')}]; Actual: [${path.vertices.map(v => v.id).join(' -> ')}]`)
|
throw Error(dedent`
|
||||||
|
Paths are different!
|
||||||
|
Expected: [${expectedV.join(' -> ')}]
|
||||||
|
Actual: [${path.vertices.map(v => v.id).join(' -> ')}]
|
||||||
|
`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { range } from "../core/iterable.ts";
|
||||||
|
import { BinaryHeap, Queue, Stack } from "../tools/structures.ts"
|
||||||
|
|
||||||
|
import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts"
|
||||||
|
|
||||||
|
Deno.test("Data structures - Stack", () => {
|
||||||
|
const stack = new Stack([1, 2, 3])
|
||||||
|
stack.push(4)
|
||||||
|
stack.push(5)
|
||||||
|
|
||||||
|
for (const i of range(5, 0)) {
|
||||||
|
assertEquals(stack.pop(), i)
|
||||||
|
}
|
||||||
|
assertEquals(stack.length, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
Deno.test("Data structures - Queue", () => {
|
||||||
|
const queue = new Queue([0, 1, 2, 3])
|
||||||
|
queue.push(4)
|
||||||
|
queue.push(5)
|
||||||
|
|
||||||
|
for (const i of range(6)) {
|
||||||
|
assertEquals(queue.pop(), i)
|
||||||
|
}
|
||||||
|
assertEquals(queue.length, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
Deno.test("Data structures - Binary Heap: Bubble up", () => {
|
||||||
|
const heap = new BinaryHeap<number>((a, b) => a - b)
|
||||||
|
|
||||||
|
heap.push(5)
|
||||||
|
heap.push(3)
|
||||||
|
|
||||||
|
assertEquals(heap.peek(), 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
Deno.test("Data structures - Binary Heap", () => {
|
||||||
|
const heap = new BinaryHeap(
|
||||||
|
(a, b) => a - b,
|
||||||
|
[5, 3, 8, 1, 2, 6, 9, 4, 7]
|
||||||
|
)
|
||||||
|
let i = 1
|
||||||
|
for (const el of heap) {
|
||||||
|
assertEquals(el, i++)
|
||||||
|
}
|
||||||
|
assertEquals(i, 10)
|
||||||
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Graph, Vertex, VertexType, EdgeType, Path, PathType } from "./graph.ts"
|
import { Graph, Vertex, VertexType, Edge, EdgeType, Path, PathType } from "./graph.ts"
|
||||||
import { Queue, Stack } from "../structures.ts"
|
import { Queue, Stack } from "../structures.ts"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,7 +43,7 @@ export class GraphSolver<vData, eData> {
|
|||||||
const path = backPaths.get(v)!.copy()
|
const path = backPaths.get(v)!.copy()
|
||||||
path.addEdge(E)
|
path.addEdge(E)
|
||||||
backPaths.set(w, path)
|
backPaths.set(w, path)
|
||||||
queue.enqueue(w)
|
queue.push(w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,8 +102,8 @@ export class GraphSolver<vData, eData> {
|
|||||||
const seenR = new Set<Vertex<vData, eData>>([right])
|
const seenR = new Set<Vertex<vData, eData>>([right])
|
||||||
|
|
||||||
while (qL.length && qR.length) {
|
while (qL.length && qR.length) {
|
||||||
const l = qL.dequeue()!
|
const l = qL.pop()!
|
||||||
const r = qR.dequeue()!
|
const r = qR.pop()!
|
||||||
|
|
||||||
// The left side will traverse the graph normally,
|
// The left side will traverse the graph normally,
|
||||||
// following the direction of the arrows in the edges
|
// following the direction of the arrows in the edges
|
||||||
@@ -115,7 +115,7 @@ export class GraphSolver<vData, eData> {
|
|||||||
const path = backL.get(l)!.copy()
|
const path = backL.get(l)!.copy()
|
||||||
path.addEdge(E)
|
path.addEdge(E)
|
||||||
backL.set(w, path)
|
backL.set(w, path)
|
||||||
qL.enqueue(w)
|
qL.push(w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ export class GraphSolver<vData, eData> {
|
|||||||
const path = backR.get(r)!.copy()
|
const path = backR.get(r)!.copy()
|
||||||
path.addEdge(E)
|
path.addEdge(E)
|
||||||
backR.set(w, path)
|
backR.set(w, path)
|
||||||
qR.enqueue(w)
|
qR.push(w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,4 +148,59 @@ export class GraphSolver<vData, eData> {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts the edges by their weight ascending, then performs {@link kruskalPresorted | Kruskal's algorithm},
|
||||||
|
* an algorithm for determining a minimum-spanning forest of a graph.
|
||||||
|
*
|
||||||
|
* Kruskal's greedy algorithm determines a minimum-spanning forest of a weighted, *undirected* graph.
|
||||||
|
*
|
||||||
|
* If you only wish to sort the edges, use {@link sortEdgesByWeight} instead.
|
||||||
|
* @author MindfulMinun
|
||||||
|
* @since 2023-03-30
|
||||||
|
*/
|
||||||
|
kruskalWithSort() {
|
||||||
|
this.sortEdgesByWeight()
|
||||||
|
return this.kruskalPresorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs Kruskal's algorithm, assuming that the edges have been presorted.
|
||||||
|
* This algorithm returns a set of the edges that span the graph.
|
||||||
|
*
|
||||||
|
* Kruskal's greedy algorithm determines a minimum-spanning forest of a weighted, *undirected* graph.
|
||||||
|
*
|
||||||
|
* This method assumes that the edges have been sorted by their weight ascending, allowing it to run in `O(m)` time.
|
||||||
|
* If the edges have not been sorted, use {@link kruskalWithSort} instead.
|
||||||
|
* @author MindfulMinun
|
||||||
|
* @since 2023-03-30
|
||||||
|
*/
|
||||||
|
kruskalPresorted(edges?: Edge<vData, eData>[]) {
|
||||||
|
const forest = new Set<Edge<vData, eData>>()
|
||||||
|
/** Keeps track of the reachable vertices. Used to ensure that newly added edges do not create a cycle. */
|
||||||
|
const reach = new WeakSet<Vertex<vData, eData>>()
|
||||||
|
const ordered: Iterable<Edge<vData, eData>> = edges ?? this.G.edges.values()
|
||||||
|
|
||||||
|
for (const E of ordered) {
|
||||||
|
if (forest.has(E)) continue
|
||||||
|
if (reach.has(E.u) && reach.has(E.v)) continue
|
||||||
|
forest.add(E)
|
||||||
|
reach.add(E.u).add(E.v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return forest
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts edges by their weight ascending. Useful for speeding up algorithms such as Kruskal's.
|
||||||
|
* This algorithm runs in `O(n log n)` time
|
||||||
|
* @author MindfulMinun
|
||||||
|
* @since 2023-03-30
|
||||||
|
*/
|
||||||
|
sortEdgesByWeight() {
|
||||||
|
const edges = Array.from(this.G.edges.values())
|
||||||
|
.sort((a, b) => this.G.weights(a) - this.G.weights(b))
|
||||||
|
.map(e => [e.id, e] as const)
|
||||||
|
this.G.edges = new Map(edges)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+13
-9
@@ -1,6 +1,6 @@
|
|||||||
import * as Colors from "https://deno.land/[email protected]/fmt/colors.ts"
|
import * as Colors from "https://deno.land/[email protected]/fmt/colors.ts"
|
||||||
|
|
||||||
type UUID = ReturnType<typeof crypto.randomUUID>
|
type UUID = string
|
||||||
|
|
||||||
export type VertexType<G> = G extends Graph<infer T, infer V> ? Vertex<T, V> : never
|
export type VertexType<G> = G extends Graph<infer T, infer V> ? Vertex<T, V> : never
|
||||||
export type EdgeType<G> = G extends Graph<infer T, infer V> ? Edge<T, V> : never
|
export type EdgeType<G> = G extends Graph<infer T, infer V> ? Edge<T, V> : never
|
||||||
@@ -25,9 +25,9 @@ interface EdgeOpts {
|
|||||||
*/
|
*/
|
||||||
export class Graph<vData, eData> {
|
export class Graph<vData, eData> {
|
||||||
/** A map of the vertices in the graph. To add a new vertex, please use the {@link Graph.createVertex} method instead. */
|
/** A map of the vertices in the graph. To add a new vertex, please use the {@link Graph.createVertex} method instead. */
|
||||||
readonly vertices: Map<UUID, Vertex<vData, eData>>
|
vertices: Map<UUID, Vertex<vData, eData>>
|
||||||
/** A map of the edges in the graph. To add a new edge, please use the {@link Graph.createEdge} method instead. */
|
/** A map of the edges in the graph. To add a new edge, please use the {@link Graph.createEdge} method instead. */
|
||||||
readonly edges: Map<UUID, Edge<vData, eData>>
|
edges: Map<UUID, Edge<vData, eData>>
|
||||||
/**
|
/**
|
||||||
* Whether or not edges created in the graph are directed when the value is omitted.
|
* Whether or not edges created in the graph are directed when the value is omitted.
|
||||||
* Note that edges created with the `directed` option will use that value instead.
|
* Note that edges created with the `directed` option will use that value instead.
|
||||||
@@ -149,26 +149,30 @@ export class Graph<vData, eData> {
|
|||||||
* @since 2022-07-26
|
* @since 2022-07-26
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class Vertex<VertexData, EdgeData> {
|
export class Vertex<vData, eData> {
|
||||||
/* The graph this vertex belongs to */
|
/* The graph this vertex belongs to */
|
||||||
graph: Graph<VertexData, EdgeData>
|
graph: Graph<vData, eData>
|
||||||
/* The value stored in this vertex, up to you! */
|
/* The value stored in this vertex, up to you! */
|
||||||
data: VertexData
|
data: vData
|
||||||
/* The unique identifier of this vertex */
|
/* The unique identifier of this vertex */
|
||||||
id: UUID
|
id: UUID
|
||||||
/* The edges connected to this vertex */
|
/* The edges connected to this vertex */
|
||||||
adjacentEdges: Set<Edge<VertexData, EdgeData>>
|
adjacentEdges: Set<Edge<vData, eData>>
|
||||||
|
|
||||||
constructor(parentGraph: Graph<VertexData, EdgeData>, id: UUID, data: VertexData) {
|
constructor(parentGraph: Graph<vData, eData>, id: UUID, data: vData) {
|
||||||
this.graph = parentGraph
|
this.graph = parentGraph
|
||||||
this.data = data
|
this.data = data
|
||||||
this.id = id
|
this.id = id
|
||||||
this.adjacentEdges = new Set()
|
this.adjacentEdges = new Set()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove this vertex from the graph. Edges connected to this vertex will also be removed.
|
||||||
|
* Callers should discard this vertex after calling this method.
|
||||||
|
*/
|
||||||
delete() { this.graph.deleteVertex(this) }
|
delete() { this.graph.deleteVertex(this) }
|
||||||
|
|
||||||
toJSON(replacer: (this: Graph<VertexData, EdgeData>, data: VertexData) => unknown = data => data) {
|
toJSON(replacer: (this: Graph<vData, eData>, data: vData) => unknown = data => data) {
|
||||||
return [this.id, replacer.call(this.graph, this.data)]
|
return [this.id, replacer.call(this.graph, this.data)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+169
-88
@@ -1,15 +1,22 @@
|
|||||||
import { fill } from "../../core/iterable.ts"
|
import { fill } from "../../core/iterable.ts"
|
||||||
import * as Colors from "https://deno.land/[email protected]/fmt/colors.ts"
|
import * as Colors from "https://deno.land/[email protected]/fmt/colors.ts"
|
||||||
|
import { Memo } from "../structures.ts"
|
||||||
|
|
||||||
class Matrix {
|
|
||||||
|
const memodDeterminant = new Memo({
|
||||||
|
fn: (M: Matrix) => M.calculateDeterminant(),
|
||||||
|
hash: M => M.toString()
|
||||||
|
}).fn
|
||||||
|
|
||||||
|
export class Matrix<M extends number = number, N extends number = number> {
|
||||||
/** The number of rows in this matrix. */
|
/** The number of rows in this matrix. */
|
||||||
m: number
|
m: M
|
||||||
/** The number of columns in this matrix. */
|
/** The number of columns in this matrix. */
|
||||||
n: number
|
n: N
|
||||||
/** The values stored in this matrix, in row-major order. */
|
/** The values stored in this matrix, in row-major order. */
|
||||||
entries: number[]
|
entries: number[]
|
||||||
|
|
||||||
constructor(m: number, n: number, entries: number[]) {
|
constructor(m: M, n: N, entries: number[]) {
|
||||||
const expectedLength = m * n
|
const expectedLength = m * n
|
||||||
if (entries.length !== expectedLength) {
|
if (entries.length !== expectedLength) {
|
||||||
throw new Error(`Expected ${expectedLength} entries, got ${entries.length}`)
|
throw new Error(`Expected ${expectedLength} entries, got ${entries.length}`)
|
||||||
@@ -19,28 +26,6 @@ class Matrix {
|
|||||||
this.entries = entries
|
this.entries = entries
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Performs naive matrix multiplication. */
|
|
||||||
rightMultiply(right: Matrix) {
|
|
||||||
if (this.n !== right.m) {
|
|
||||||
throw new Error(`Cannot multiply ${this.m}x${this.n} matrix by ${right.m}x${right.n} matrix.`)
|
|
||||||
}
|
|
||||||
const result = Matrix.null(this.m, right.n)
|
|
||||||
for (let i = 0; i < this.m; i++) {
|
|
||||||
for (let j = 0; j < right.n; j++) {
|
|
||||||
let sum = 0
|
|
||||||
for (let k = 0; k < this.n; k++) {
|
|
||||||
sum += this.get(i, k) * right.get(k, j)
|
|
||||||
}
|
|
||||||
result.set(i, j, sum)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
leftMultiply(left: Matrix) {
|
|
||||||
return left.rightMultiply(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
set(row: number, col: number, value: number) {
|
set(row: number, col: number, value: number) {
|
||||||
const offset = this.#calculateOffset(row, col)
|
const offset = this.#calculateOffset(row, col)
|
||||||
this.entries[offset] = value
|
this.entries[offset] = value
|
||||||
@@ -51,47 +36,69 @@ class Matrix {
|
|||||||
return this.entries[offset]
|
return this.entries[offset]
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
|
||||||
const strs = this.entries.map(e => e.toString())
|
|
||||||
const maxLen = Math.max(...strs.map(s => s.length))
|
|
||||||
const padded = strs.map(s => s.padStart(maxLen, ' '))
|
|
||||||
|
|
||||||
let out = ""
|
|
||||||
for (let i = 0; i < this.m; i++) {
|
|
||||||
if (i != 0) out += '\n'
|
|
||||||
out += Colors.gray(i === 0 ? '┌ ' : i === this.m - 1 ? '└ ' : '│ ')
|
|
||||||
// out += Colors.gray('│ ')
|
|
||||||
|
|
||||||
for (let j = 0; j < this.n; j++) {
|
|
||||||
const offset = this.#calculateOffset(i, j)
|
|
||||||
const value = this.entries[offset]
|
|
||||||
const txt = padded[offset]
|
|
||||||
out += (value == 0 ? Colors.gray(txt) : txt) + ' '
|
|
||||||
}
|
|
||||||
out += Colors.gray(i === 0 ? '┐ ' : i === this.m - 1 ? '┘ ' : '│ ')
|
|
||||||
// out += Colors.gray('│')
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
[Symbol.for("Deno.customInspect")]() {
|
|
||||||
return this.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
#calculateOffset(row: number, col: number) {
|
|
||||||
if (!(0 <= row || row < this.m)) {
|
|
||||||
throw new Error(`Row ${row} is out of bounds for ${this.m}x${this.n} matrix.`)
|
|
||||||
}
|
|
||||||
if (!(0 <= col || col < this.n)) {
|
|
||||||
throw new Error(`Column ${col} is out of bounds for ${this.m}x${this.n} matrix.`)
|
|
||||||
}
|
|
||||||
return row * this.n + col
|
|
||||||
}
|
|
||||||
|
|
||||||
copy() {
|
copy() {
|
||||||
return new Matrix(this.m, this.n, this.entries.slice())
|
return new Matrix(this.m, this.n, this.entries.slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs naive matrix multiplication.
|
||||||
|
* @thorws If the dimensions of the matrices are incompatible.
|
||||||
|
*/
|
||||||
|
rightMultiply<P extends number, Q extends number>(right: Matrix<P, Q>) {
|
||||||
|
if (this.n as number !== right.m as number) {
|
||||||
|
throw new Error(`Cannot multiply ${this.m}x${this.n} matrix by ${right.m}x${right.n} matrix.`)
|
||||||
|
}
|
||||||
|
const result = Matrix.null(this.m, right.n)
|
||||||
|
for (let i = 0; i < this.m; i++) {
|
||||||
|
for (let j = 0; j < right.n; j++) {
|
||||||
|
let sum = 0
|
||||||
|
for (let k = 0; k < this.n; k++) {
|
||||||
|
sum += this.get(i, k) * right.get(k, j)
|
||||||
|
}
|
||||||
|
result.set(i, j, sum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result as N & P extends never ? never : Matrix<M, Q>
|
||||||
|
}
|
||||||
|
|
||||||
|
leftMultiply<P extends number, Q extends number>(left: Matrix<P, Q>): Q & M extends never ? never : Matrix<P, N> {
|
||||||
|
return left.rightMultiply(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateDeterminant<S extends N & M>(): S extends never ? number : number {
|
||||||
|
this.#assertSquare()
|
||||||
|
|
||||||
|
const n = this.m
|
||||||
|
const [a, b, c, d] = this.entries
|
||||||
|
|
||||||
|
if (n === 1) return a
|
||||||
|
if (n === 2) return a * d - b * c
|
||||||
|
|
||||||
|
// Otherwise, use Laplace expansion.
|
||||||
|
let total = 0
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
// Ignore the first row and the ith column.
|
||||||
|
const submatrix = this.laplaceExpansion(0, i)
|
||||||
|
const determinant = memodDeterminant(submatrix)
|
||||||
|
const sign = i % 2 === 0 ? 1 : -1
|
||||||
|
total += sign * this.get(0, i) * determinant
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the submatrix obtained by removing the given row and column.
|
||||||
|
*/
|
||||||
|
laplaceExpansion(row: number, col: number) {
|
||||||
|
const matrix = Matrix.null(this.m - 1, this.n - 1)
|
||||||
|
matrix.entries = this.entries.filter((_, i) => {
|
||||||
|
const r = Math.floor(i / this.n)
|
||||||
|
const c = i % this.n
|
||||||
|
return r !== row && c !== col
|
||||||
|
})
|
||||||
|
return matrix
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs Gaussian elimination on this matrix, transforming it into an upper triangular matrix.
|
* Performs Gaussian elimination on this matrix, transforming it into an upper triangular matrix.
|
||||||
* Note that this method does NOT mutate the original matrix, instead returning a new one.
|
* Note that this method does NOT mutate the original matrix, instead returning a new one.
|
||||||
@@ -101,8 +108,7 @@ class Matrix {
|
|||||||
*/
|
*/
|
||||||
toUpperTriangular() {
|
toUpperTriangular() {
|
||||||
const A = this.copy()
|
const A = this.copy()
|
||||||
const m = A.m
|
const { m, n } = A
|
||||||
const n = A.n
|
|
||||||
for (let i = 0; i < m; i++) {
|
for (let i = 0; i < m; i++) {
|
||||||
// Find the pivot row.
|
// Find the pivot row.
|
||||||
let pivotRow = i
|
let pivotRow = i
|
||||||
@@ -132,46 +138,121 @@ class Matrix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new matrix with the given dimensions and entries.
|
* Returns a submatrix of this matrix.
|
||||||
|
* @param row The row of the top-left corner of the submatrix.
|
||||||
|
* @param col The column of the top-left corner of the submatrix.
|
||||||
|
* @param m The number of rows in the submatrix.
|
||||||
|
* @param n The number of columns in the submatrix.
|
||||||
|
*/
|
||||||
|
submatrix<P extends number, Q extends number>(row: number, col: number, p: P, q: Q): Matrix<P, Q> {
|
||||||
|
const submatrix = Matrix.null(p, q)
|
||||||
|
for (let i = 0; i < p; i++) {
|
||||||
|
for (let j = 0; j < q; j++) {
|
||||||
|
submatrix.set(i, j, this.get(row + i, col + j))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return submatrix
|
||||||
|
}
|
||||||
|
|
||||||
|
#calculateOffset(row: number, col: number) {
|
||||||
|
if (!(0 <= row || row < this.m)) {
|
||||||
|
throw new Error(`Row ${row} is out of bounds for ${this.m}x${this.n} matrix.`)
|
||||||
|
}
|
||||||
|
if (!(0 <= col || col < this.n)) {
|
||||||
|
throw new Error(`Column ${col} is out of bounds for ${this.m}x${this.n} matrix.`)
|
||||||
|
}
|
||||||
|
return row * this.n + col
|
||||||
|
}
|
||||||
|
|
||||||
|
#assertSquare<S extends N & M>(): asserts this is Matrix<S, S> {
|
||||||
|
const { m, n } = this as Matrix<M, number>
|
||||||
|
if (m !== n) {
|
||||||
|
throw new Error(`Expected square matrix, got ${m}x${n} matrix.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
let out = ''
|
||||||
|
out += 'Matrix<' + this.m + ', ' + this.n + '>['
|
||||||
|
out += this.entries.map(e => e.toString()).join(', ')
|
||||||
|
out += ']'
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.for("Deno.customInspect")]() {
|
||||||
|
const strs = this.entries.map(e => e.toString())
|
||||||
|
const maxLengths = Array.from(fill(0, this.n))
|
||||||
|
|
||||||
|
// Find the maximum length of the strings in each column.
|
||||||
|
for (let i = 0; i < this.m; i++) {
|
||||||
|
for (let j = 0; j < this.n; j++) {
|
||||||
|
const offset = this.#calculateOffset(i, j)
|
||||||
|
maxLengths[j] = Math.max(maxLengths[j], strs[offset].length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad each string to the maximum length.
|
||||||
|
const padded = strs.map((s, i) => s.padStart(maxLengths[i % this.n], ' '))
|
||||||
|
|
||||||
|
// Build the string.
|
||||||
|
let out = ""
|
||||||
|
for (let i = 0; i < this.m; i++) {
|
||||||
|
if (i != 0) out += '\n'
|
||||||
|
out += Colors.gray(i === 0 ? '┌ ' : i === this.m - 1 ? '└ ' : '│ ')
|
||||||
|
|
||||||
|
for (let j = 0; j < this.n; j++) {
|
||||||
|
const offset = this.#calculateOffset(i, j)
|
||||||
|
const value = this.entries[offset]
|
||||||
|
const txt = padded[offset]
|
||||||
|
out += (value == 0 ? Colors.gray(txt) : txt) + ' '
|
||||||
|
}
|
||||||
|
out += Colors.gray(i === 0 ? '┐ ' : i === this.m - 1 ? '┘ ' : '│ ')
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new matrix with the given numbers and entries.
|
||||||
* All entries are initialized to 0.
|
* All entries are initialized to 0.
|
||||||
*/
|
*/
|
||||||
static null(m: number, n: number) {
|
static null<M extends number, N extends number>(m: M, n: N) {
|
||||||
const zeroes = Array.from(fill(0, m * n))
|
const zeroes = Array.from(fill(0, m * n))
|
||||||
return new Matrix(m, n, zeroes)
|
return new Matrix(m, n, zeroes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a square identity matrix with the given dimension.
|
* Creates a square identity matrix with the given number.
|
||||||
* An identity matrix is a square matrix with 1s on the diagonal and 0s everywhere else.
|
* An identity matrix is a square matrix with 1s on the diagonal and 0s everywhere else.
|
||||||
*/
|
*/
|
||||||
static identity(n: number) {
|
static identity<N extends number>(n: N): Matrix<N, N> {
|
||||||
const I = Matrix.null(n, n)
|
const I = Matrix.null(n, n)
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
I.set(i, i, 1)
|
I.set(i, i, 1)
|
||||||
}
|
}
|
||||||
return I
|
return I
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dr. Strang's favorite matrix.
|
||||||
|
* Returns a tridiagonal matrix with 2s on the diagonal and -1s on the sub- and super-diagonals.
|
||||||
|
*/
|
||||||
|
static strang<N extends number>(n: N): Matrix<N, N> {
|
||||||
|
const S = Matrix.null(n, n)
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
S.set(i, i, 2)
|
||||||
|
if (i > 0) S.set(i, i - 1, -1)
|
||||||
|
if (i < n - 1) S.set(i, i + 1, -1)
|
||||||
|
}
|
||||||
|
return S
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (import.meta.main) {
|
if (import.meta.main) {
|
||||||
// const A = new Matrix(3, 3, [
|
const A = Matrix.strang(15)
|
||||||
// 2, -1, 0,
|
|
||||||
// -1, 2, -1,
|
console.time('calculateDeterminant')
|
||||||
// 0, -3, 4
|
console.log(A.calculateDeterminant())
|
||||||
// ])
|
console.timeEnd('calculateDeterminant')
|
||||||
const A = new Matrix(4, 4, [
|
|
||||||
16, 2, 3, 13,
|
|
||||||
5 , 11, 10, 8,
|
|
||||||
9 , 7, 6, 12,
|
|
||||||
4 , 14, 15, 1,
|
|
||||||
])
|
|
||||||
const x = new Matrix(3, 1, [0, 0, 1])
|
|
||||||
// const A = Matrix.identity(10)
|
|
||||||
|
|
||||||
console.log(A)
|
|
||||||
console.log(A.toUpperTriangular())
|
|
||||||
// console.log(x)
|
|
||||||
// const b = A.rightMultiply(x)
|
|
||||||
// console.log(b)
|
|
||||||
}
|
}
|
||||||
+3
-88
@@ -1,89 +1,4 @@
|
|||||||
/**
|
console.error("Hey there, past me! Please don't import `structures.ts`, instead import the specific structure you need.")
|
||||||
* Data structure allowing for the insertion and removal of
|
|
||||||
* elements in a LIFO manner in `O(1)` time.
|
|
||||||
* @author MindfulMinun
|
|
||||||
* @since 2022-10-22
|
|
||||||
*/
|
|
||||||
export class Stack<T> implements Iterable<T> {
|
|
||||||
#elements: Record<number, T>
|
|
||||||
#head: number
|
|
||||||
|
|
||||||
constructor(initials?: Iterable<T>) {
|
export * from './structures/Queue.ts'
|
||||||
this.#elements = {}
|
export * from './structures/Stack.ts'
|
||||||
this.#head = 0
|
|
||||||
if (!initials) return
|
|
||||||
for (const el of initials) this.push(el)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Add an element to the stack */
|
|
||||||
push(element: T): void {
|
|
||||||
this.#elements[this.#head++] = element
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Remove an element from the stack and return it. */
|
|
||||||
pop(): T | undefined {
|
|
||||||
if (this.length === 0) return undefined
|
|
||||||
const el = this.#elements[--this.#head]
|
|
||||||
delete this.#elements[this.#head]
|
|
||||||
return el
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Preview the topmost element of the stack without removing it. */
|
|
||||||
peek(): T | undefined {
|
|
||||||
if (this.length === 0) return undefined
|
|
||||||
return this.#elements[this.#head - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The number of elements remaining in the stack */
|
|
||||||
get length() { return this.#head }
|
|
||||||
|
|
||||||
*[Symbol.iterator]() {
|
|
||||||
while (this.length !== 0) yield this.pop()!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data structure allowing for the insertion and removal of
|
|
||||||
* elements in a FIFO manner in `O(1)` time.
|
|
||||||
* @author MindfulMinun
|
|
||||||
* @since 2022-10-22
|
|
||||||
*/
|
|
||||||
export class Queue<T> implements Iterable<T> {
|
|
||||||
#elements: Record<number, T>
|
|
||||||
#head: number
|
|
||||||
#tail: number
|
|
||||||
|
|
||||||
constructor(initials?: Iterable<T>) {
|
|
||||||
this.#elements = {}
|
|
||||||
this.#head = 0
|
|
||||||
this.#tail = 0
|
|
||||||
if (!initials) return
|
|
||||||
for (const el of initials) this.enqueue(el)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Add an element to the queue */
|
|
||||||
enqueue(element: T): void {
|
|
||||||
this.#elements[this.#tail++] = element
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Remove an element from the queue and return it. */
|
|
||||||
dequeue(): T | undefined {
|
|
||||||
if (this.length === 0) return undefined
|
|
||||||
const el = this.#elements[this.#head]
|
|
||||||
delete this.#elements[this.#head++]
|
|
||||||
return el
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Preview the next element in the queue without removing it. */
|
|
||||||
peek(): T | undefined {
|
|
||||||
if (this.length === 0) return undefined
|
|
||||||
return this.#elements[this.#head]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The number of elements remaining in the queue */
|
|
||||||
get length() { return this.#tail - this.#head }
|
|
||||||
|
|
||||||
*[Symbol.iterator]() {
|
|
||||||
while (this.length !== 0) yield this.dequeue()!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { swap } from '../../core/iterable.ts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A minimum binary heap.
|
||||||
|
*
|
||||||
|
* A binary heap always satisfies the following properties:
|
||||||
|
* - The root node is the minimum element in the heap.
|
||||||
|
* - The children of a node are always greater than or equal to that node.
|
||||||
|
*
|
||||||
|
* @author MindfulMinun
|
||||||
|
* @since 2023-03-10
|
||||||
|
*/
|
||||||
|
export class BinaryHeap<T> implements Iterable<T> {
|
||||||
|
#comparator: (a: T, b: T) => number
|
||||||
|
#elements: T[]
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
comparator: (a: T, b: T) => number,
|
||||||
|
initials: Iterable<T> = []
|
||||||
|
) {
|
||||||
|
this.#comparator = comparator
|
||||||
|
this.#elements = []
|
||||||
|
|
||||||
|
for (const el of initials) this.push(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
*[Symbol.iterator]() {
|
||||||
|
while (this.length !== 0) yield this.pop()!
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert an element into the heap.
|
||||||
|
*/
|
||||||
|
push(element: T): void {
|
||||||
|
this.#elements.push(element)
|
||||||
|
this.#bubbleUp(this.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the minimum element from the heap and return it.
|
||||||
|
*/
|
||||||
|
pop(): T | undefined {
|
||||||
|
if (this.length === 0) return undefined
|
||||||
|
|
||||||
|
// Swap the last element with the root
|
||||||
|
swap(this.#elements, 0, this.length - 1)
|
||||||
|
// Remove the last element
|
||||||
|
const el = this.#elements.pop()
|
||||||
|
// Bubble the new root down
|
||||||
|
this.#bubbleDown(0)
|
||||||
|
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Preview the minimum element in the heap without removing it. */
|
||||||
|
peek(): T | undefined {
|
||||||
|
if (this.length === 0) return undefined
|
||||||
|
return this.#elements[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bubble an element up the heap until it is in the correct position. */
|
||||||
|
#bubbleUp(index: number): void {
|
||||||
|
// If the element is the root, it is in the correct position
|
||||||
|
if (index === 0) return
|
||||||
|
|
||||||
|
// Bubble the element up the heap until it is in the correct position
|
||||||
|
let current = index
|
||||||
|
let parent = BinaryHeap.parent(current)
|
||||||
|
const val = this.#elements[current]
|
||||||
|
|
||||||
|
// While the element is less than its parent, swap it with its parent
|
||||||
|
while (0 < current && this.#comparator(val, this.#elements[parent]) < 0) {
|
||||||
|
this.#elements[current] = this.#elements[parent]
|
||||||
|
current = parent
|
||||||
|
parent = BinaryHeap.parent(current)
|
||||||
|
}
|
||||||
|
this.#elements[current] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bubble an element down the heap until it is in the correct position. */
|
||||||
|
#bubbleDown(index: number): void {
|
||||||
|
// If the element is a leaf, it is in the correct position
|
||||||
|
if (index >= this.length) return
|
||||||
|
|
||||||
|
// Bubble the element down the heap until it is in the correct position
|
||||||
|
let current = index
|
||||||
|
let [left, right] = BinaryHeap.leftRight(current)
|
||||||
|
const val = this.#elements[current]
|
||||||
|
|
||||||
|
// While the element is greater than its children, swap it with its smallest child
|
||||||
|
while (left < this.length) {
|
||||||
|
// Find the smallest child
|
||||||
|
let smallest = left
|
||||||
|
if (right < this.length && 0 < this.#comparator(this.#elements[left], this.#elements[right])) {
|
||||||
|
smallest = right
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the element is smaller than its smallest child, it is in the correct position
|
||||||
|
if (this.#comparator(val, this.#elements[smallest]) <= 0) break
|
||||||
|
|
||||||
|
// Swap the element with its smallest child
|
||||||
|
this.#elements[current] = this.#elements[smallest]
|
||||||
|
current = smallest
|
||||||
|
;[left, right] = BinaryHeap.leftRight(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#elements[current] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
private static leftRight(index: number) {
|
||||||
|
return [index * 2 + 1, index * 2 + 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parent(index: number) {
|
||||||
|
return Math.floor((index - 1) / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The number of elements in the heap */
|
||||||
|
get length() { return this.#elements.length }
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* A cache-like object that can be used with {@link Memo}. This interface is
|
||||||
|
* compatible with `Map`, but it can also be used to implement other caching
|
||||||
|
* strategies, such as an LRU cache.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Cachelike<T> {
|
||||||
|
get(key: string): T | undefined
|
||||||
|
set(key: string, value: T): void
|
||||||
|
has(key: string): boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemoOpts<A extends unknown[], R extends unknown> {
|
||||||
|
fn: (...args: A) => R
|
||||||
|
hash?: (...args: A) => string
|
||||||
|
cache?: Cachelike<R>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a function into a memo.
|
||||||
|
*
|
||||||
|
* A memo is a function that caches its results, allowing for faster
|
||||||
|
* computation of the same function with the same arguments.
|
||||||
|
*
|
||||||
|
* The memo can be configured with a custom hash function and cache.
|
||||||
|
* By default, the hash function is `JSON.stringify` and the cache is a `Map`.
|
||||||
|
*
|
||||||
|
* Use the memo by calling `memo.fn(...args)` on the memo object.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author MindfulMinun
|
||||||
|
* @since 2023-06-16
|
||||||
|
*/
|
||||||
|
export class Memo<A extends unknown[], R extends unknown> {
|
||||||
|
#ogfn: (...args: A) => R
|
||||||
|
hash: (...args: A) => string
|
||||||
|
cache: Cachelike<R>
|
||||||
|
|
||||||
|
hits: number
|
||||||
|
misses: number
|
||||||
|
|
||||||
|
constructor(fn: (...args: A) => R)
|
||||||
|
constructor(opts: MemoOpts<A, R>)
|
||||||
|
constructor(x: MemoOpts<A, R> | ((...args: A) => R)) {
|
||||||
|
this.#ogfn = () => { throw new Error('Memo not initialized') }
|
||||||
|
this.hash = (...args) => JSON.stringify(args)
|
||||||
|
this.cache = new Map<string, R>()
|
||||||
|
this.hits = 0
|
||||||
|
this.misses = 0
|
||||||
|
|
||||||
|
if (typeof x === 'function') {
|
||||||
|
this.#ogfn = x
|
||||||
|
} else {
|
||||||
|
this.#ogfn = x.fn
|
||||||
|
this.hash = x.hash ?? this.hash
|
||||||
|
this.cache = x.cache ?? this.cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#memoHandler(...args: A): R {
|
||||||
|
const key = this.hash.apply(this, args)
|
||||||
|
if (this.cache.has(key)) {
|
||||||
|
this.hits++
|
||||||
|
return this.cache.get(key)!
|
||||||
|
}
|
||||||
|
const result = this.#ogfn.apply(this, args)
|
||||||
|
this.cache.set(key, result)
|
||||||
|
this.misses++
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
get ratio() {
|
||||||
|
return this.hits / (this.hits + this.misses)
|
||||||
|
}
|
||||||
|
|
||||||
|
get fn() {
|
||||||
|
return this.#memoHandler.bind(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
const fib = new Memo({
|
||||||
|
fn: (n: number): number => {
|
||||||
|
switch (n) {
|
||||||
|
case 0: case 1: return n
|
||||||
|
default: return fib(n - 1) + fib(n - 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).fn
|
||||||
|
|
||||||
|
console.log(fib(1000))
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Data structure allowing for the insertion and removal of
|
||||||
|
* elements in a FIFO manner in `O(1)` time.
|
||||||
|
* @author MindfulMinun
|
||||||
|
* @since 2022-10-22
|
||||||
|
*/
|
||||||
|
export class Queue<T> implements Iterable<T> {
|
||||||
|
#elements: Record<number, T>
|
||||||
|
#head: number
|
||||||
|
#tail: number
|
||||||
|
|
||||||
|
constructor(initials?: Iterable<T>) {
|
||||||
|
this.#elements = {}
|
||||||
|
this.#head = 0
|
||||||
|
this.#tail = 0
|
||||||
|
if (!initials) return
|
||||||
|
for (const el of initials) this.push(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add an element to the queue */
|
||||||
|
push(element: T): void {
|
||||||
|
this.#elements[this.#tail++] = element
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove an element from the queue and return it. */
|
||||||
|
pop(): T | undefined {
|
||||||
|
if (this.length === 0) return undefined
|
||||||
|
const el = this.#elements[this.#head]
|
||||||
|
delete this.#elements[this.#head++]
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Preview the next element in the queue without removing it. */
|
||||||
|
peek(): T | undefined {
|
||||||
|
if (this.length === 0) return undefined
|
||||||
|
return this.#elements[this.#head]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The number of elements remaining in the queue */
|
||||||
|
get length() { return this.#tail - this.#head }
|
||||||
|
|
||||||
|
*[Symbol.iterator]() {
|
||||||
|
while (this.length !== 0) yield this.pop()!
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Data structure allowing for the insertion and removal of
|
||||||
|
* elements in a LIFO manner in `O(1)` time.
|
||||||
|
* @author MindfulMinun
|
||||||
|
* @since 2022-10-22
|
||||||
|
*/
|
||||||
|
export class Stack<T> implements Iterable<T> {
|
||||||
|
#elements: Record<number, T>
|
||||||
|
#head: number
|
||||||
|
|
||||||
|
constructor(initials?: Iterable<T>) {
|
||||||
|
this.#elements = {}
|
||||||
|
this.#head = 0
|
||||||
|
if (!initials) return
|
||||||
|
for (const el of initials) this.push(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add an element to the stack */
|
||||||
|
push(element: T): void {
|
||||||
|
this.#elements[this.#head++] = element
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove an element from the stack and return it. */
|
||||||
|
pop(): T | undefined {
|
||||||
|
if (this.length === 0) return undefined
|
||||||
|
const el = this.#elements[--this.#head]
|
||||||
|
delete this.#elements[this.#head]
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Preview the topmost element of the stack without removing it. */
|
||||||
|
peek(): T | undefined {
|
||||||
|
if (this.length === 0) return undefined
|
||||||
|
return this.#elements[this.#head - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The number of elements remaining in the stack */
|
||||||
|
get length() { return this.#head }
|
||||||
|
|
||||||
|
*[Symbol.iterator]() {
|
||||||
|
while (this.length !== 0) yield this.pop()!
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
type Transition<State extends string | number | symbol, InputAlphabet, Context> = (
|
||||||
|
this: StateMachine<State, InputAlphabet, Context>,
|
||||||
|
input: InputAlphabet,
|
||||||
|
machine: StateMachine<State, InputAlphabet, Context>
|
||||||
|
) => void
|
||||||
|
|
||||||
|
class StateMachine<State extends string | number | symbol = 0, InputAlphabet = string, Context = null> {
|
||||||
|
#initialState: State
|
||||||
|
#transitionTable: Partial<Record<State, Transition<State, InputAlphabet, Context>>>
|
||||||
|
#acceptStates: Set<State>
|
||||||
|
#contextFactory: () => Context
|
||||||
|
currentState!: State
|
||||||
|
context!: Context
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
initialState: State,
|
||||||
|
transitionTable: Partial<Record<State, Transition<State, InputAlphabet, Context>>>,
|
||||||
|
acceptStates: Iterable<State> = [],
|
||||||
|
contextFactory: () => Context = () => (null as Context)
|
||||||
|
) {
|
||||||
|
this.#initialState = initialState
|
||||||
|
this.#transitionTable = transitionTable
|
||||||
|
this.#acceptStates = new Set(acceptStates)
|
||||||
|
this.#contextFactory = contextFactory
|
||||||
|
this.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
runWithInput(elements: Iterable<InputAlphabet>) {
|
||||||
|
for (const element of elements) {
|
||||||
|
this.transitionWith(element)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
transitionWith(input: InputAlphabet) {
|
||||||
|
const fn = this.#transitionTable[this.currentState]
|
||||||
|
if (!fn) throw new Error(`No transition function for state ${String(this.currentState)}`)
|
||||||
|
fn.call(this, input, this)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
goto(state: State) {
|
||||||
|
this.currentState = state
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
accepts(state = this.currentState) { return this.#acceptStates.has(state) }
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.currentState = this.#initialState
|
||||||
|
this.context = this.#contextFactory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
// S1: initial state, accept.
|
||||||
|
// 0 -> S2
|
||||||
|
// 1 -> S1
|
||||||
|
// S2: reject.
|
||||||
|
// 0 -> S1
|
||||||
|
// 1 -> S2
|
||||||
|
|
||||||
|
const enum MyState {
|
||||||
|
P = 'P',
|
||||||
|
R = 'R',
|
||||||
|
N = 'N',
|
||||||
|
D = 'D',
|
||||||
|
L = 'L'
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum Direction {
|
||||||
|
UP = 'UP',
|
||||||
|
DOWN = 'DOWN',
|
||||||
|
}
|
||||||
|
|
||||||
|
const M = new StateMachine<MyState, Direction>(MyState.P, {
|
||||||
|
[MyState.P]: (input, m) => input === Direction.UP ? m.goto(MyState.P) : m.goto(MyState.R),
|
||||||
|
[MyState.R]: (input, m) => input === Direction.UP ? m.goto(MyState.P) : m.goto(MyState.N),
|
||||||
|
[MyState.N]: (input, m) => input === Direction.UP ? m.goto(MyState.R) : m.goto(MyState.D),
|
||||||
|
[MyState.D]: (input, m) => input === Direction.UP ? m.goto(MyState.N) : m.goto(MyState.L),
|
||||||
|
[MyState.L]: (input, m) => input === Direction.UP ? m.goto(MyState.D) : m.goto(MyState.L),
|
||||||
|
}, [MyState.P])
|
||||||
|
// const M = new StateMachine<MyState, '1' | '0'>(MyState.even, {
|
||||||
|
// [MyState.even]: (input, m) => input === '1' ? m.goto(MyState.even) : m.goto(MyState.odd),
|
||||||
|
// [MyState.odd ]: (input, m) => input === '0' ? m.goto(MyState.even) : m.goto(MyState.odd),
|
||||||
|
// }, [MyState.even])
|
||||||
|
|
||||||
|
M.runWithInput([
|
||||||
|
Direction.DOWN,
|
||||||
|
Direction.DOWN,
|
||||||
|
Direction.DOWN,
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log(M)
|
||||||
|
console.log(M.accepts())
|
||||||
|
}
|
||||||
Reference in new issue
Block a user